Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# The cookiecutter scaffold under plugin-template/ contains Jinja placeholders
# (e.g. {{ cookiecutter.* }} in file paths, .py, and .toml) that are not valid
# Python/TOML, so black/ruff/check-ast and the other hooks cannot process them.
# Exclude the whole directory from every hook.
exclude: '^plugin-template/'

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
Expand Down
21 changes: 21 additions & 0 deletions plugin-template/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright (c) 2024 Snowflake Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# This cookiecutter template is not part of the snowflake-cli test suite. Its
# files contain Jinja placeholders (`{{ cookiecutter.* }}`) and are not valid
# Python, so the repo-wide pytest runs that collect from the project root
# (e.g. `pytest -m performance`, `pytest -m e2e`) would fail trying to import
# them. Ignore everything under this directory during collection; the generated
# project ships its own valid tests.
collect_ignore_glob = ["*"]
13 changes: 13 additions & 0 deletions plugin-template/cookiecutter.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"plugin_name": "my-snow-plugin",
"plugin_module": "{{ cookiecutter.plugin_name | replace('-', '_') }}",
Comment thread
sfc-gh-olorek marked this conversation as resolved.
"package_namespace": "snowflakecli_plugins",
"cli_command_name": "{{ cookiecutter.plugin_name }}",
"cli_parent_path": "",
"plugin_description": "A Snowflake CLI plugin.",
"author_name": "Your Name",
"author_email": "you@example.com",
"requires_connection": true,
"command_type": ["group", "single"],
"python_requires": ">=3.10"
}
53 changes: 53 additions & 0 deletions plugin-template/{{cookiecutter.plugin_name}}/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<!--
Copyright (c) 2024 Snowflake Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

# {{ cookiecutter.plugin_name }}

{{ cookiecutter.plugin_description }}

## Development

### Install in development mode

```bash
pip install -e .
```

### Enable the plugin

```bash
snow plugin enable {{ cookiecutter.plugin_module }}
```

### Run tests

```bash
pytest
```

## Plugin Structure

| File | Purpose |
|------|---------|
| `interface.py` | Command surface definition (spec + handler ABC) -- reviewed in Phase 1 |
| `handler.py` | Business logic implementation -- reviewed in Phase 2 |
| `plugin_spec.py` | Wires interface + handler into Snowflake CLI |

## Contributing

1. Modify `interface.py` to define your command surface (Phase 1 PR)
2. After approval, implement handlers in `handler.py` (Phase 2 PR)
3. Run `pytest` to validate the interface-handler contract
38 changes: 38 additions & 0 deletions plugin-template/{{cookiecutter.plugin_name}}/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Copyright (c) 2024 Snowflake Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "{{ cookiecutter.plugin_name }}"
version = "0.1.0"
description = "{{ cookiecutter.plugin_description }}"
requires-python = "{{ cookiecutter.python_requires }}"
authors = [
{ name = "{{ cookiecutter.author_name }}", email = "{{ cookiecutter.author_email }}" },
]
dependencies = [
"snowflake-cli",
]

[project.entry-points."snowflake.cli.plugin.command"]
{{ cookiecutter.plugin_module }} = "{{ cookiecutter.package_namespace }}.{{ cookiecutter.plugin_module }}.plugin_spec"

[tool.hatch.build.targets.wheel]
packages = ["src/{{ cookiecutter.package_namespace }}"]

[tool.pytest.ini_options]
testpaths = ["tests"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright (c) 2024 Snowflake Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright (c) 2024 Snowflake Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Copyright (c) 2024 Snowflake Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""{{ cookiecutter.plugin_description }} -- Implementation.

This file contains the concrete handler implementing the interface
defined in ``interface.py``. Submit this for review (Phase 2) after
the interface has been approved.
"""

from __future__ import annotations

from snowflake.cli.api.output.types import CommandResult, MessageResult

from {{ cookiecutter.package_namespace }}.{{ cookiecutter.plugin_module }}.interface import (
{{ cookiecutter.plugin_module | replace('_', ' ') | title | replace(' ', '') }}Handler,
)


class {{ cookiecutter.plugin_module | replace('_', ' ') | title | replace(' ', '') }}HandlerImpl(
{{ cookiecutter.plugin_module | replace('_', ' ') | title | replace(' ', '') }}Handler,
):
{% if cookiecutter.command_type == "group" %}
def hello(self, name: str) -> CommandResult:
return MessageResult(f"Hello, {name}!")

def status(self) -> CommandResult:
return MessageResult("Plugin is running.")
{%- else %}
def run(self, name: str) -> CommandResult:
return MessageResult(f"Hello, {name}!")
{%- endif %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Copyright (c) 2024 Snowflake Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""{{ cookiecutter.plugin_description }}

This file defines the command surface for ``snow {{ cookiecutter.cli_command_name }}``.
Submit this file for review (Phase 1) before writing the implementation.

Commands
--------
{%- if cookiecutter.command_type == "group" %}
- ``snow {{ cookiecutter.cli_command_name }} hello <name>`` -- Say hello.
- ``snow {{ cookiecutter.cli_command_name }} status`` -- Show status.
{%- else %}
- ``snow {{ cookiecutter.cli_command_name }} <name>`` -- Run the command.
{%- endif %}
"""

from __future__ import annotations

from abc import abstractmethod

from snowflake.cli.api.output.types import CommandResult
from snowflake.cli.api.plugins.command.interface import (
CommandDef,
{%- if cookiecutter.command_type == "group" %}
CommandGroupSpec,
{%- else %}
SingleCommandSpec,
{%- endif %}
CommandHandler,
ParamDef,
ParamKind,
REQUIRED,
Comment thread
sfc-gh-olorek marked this conversation as resolved.
Outdated
)

# ---------------------------------------------------------------------------
# Command surface (reviewable spec)
# ---------------------------------------------------------------------------

{% if cookiecutter.command_type == "group" -%}
PLUGIN_SPEC = CommandGroupSpec(
name="{{ cookiecutter.cli_command_name }}",
help="{{ cookiecutter.plugin_description }}",
parent_path=({{ cookiecutter.cli_parent_path | default("", true) }}),
Comment thread
sfc-gh-olorek marked this conversation as resolved.
Outdated
commands=(
CommandDef(
name="hello",
help="Say hello to someone.",
handler_method="hello",
requires_connection={{ cookiecutter.requires_connection }},
params=(
ParamDef(
name="name",
type=str,
kind=ParamKind.ARGUMENT,
help="Name to greet.",
),
),
output_type="MessageResult",
),
CommandDef(
name="status",
help="Show plugin status.",
handler_method="status",
requires_connection={{ cookiecutter.requires_connection }},
output_type="MessageResult",
),
),
)
{%- else -%}
PLUGIN_SPEC = SingleCommandSpec(
parent_path=({{ cookiecutter.cli_parent_path | default("", true) }}),
Comment thread
sfc-gh-olorek marked this conversation as resolved.
Outdated
command=CommandDef(
name="{{ cookiecutter.cli_command_name }}",
help="{{ cookiecutter.plugin_description }}",
handler_method="run",
requires_connection={{ cookiecutter.requires_connection }},
params=(
ParamDef(
name="name",
type=str,
kind=ParamKind.ARGUMENT,
help="Name argument.",
),
),
output_type="MessageResult",
),
)
{%- endif %}


# ---------------------------------------------------------------------------
# Handler contract (ABC)
# ---------------------------------------------------------------------------


class {{ cookiecutter.plugin_module | replace('_', ' ') | title | replace(' ', '') }}Handler(CommandHandler):
"""Handler contract for {{ cookiecutter.cli_command_name }} commands.

Each abstract method corresponds to a ``CommandDef`` above via
``handler_method``.
"""
{% if cookiecutter.command_type == "group" %}
@abstractmethod
def hello(self, name: str) -> CommandResult:
"""Say hello."""
...

@abstractmethod
def status(self) -> CommandResult:
"""Show status."""
...
{% else %}
@abstractmethod
def run(self, name: str) -> CommandResult:
"""Run the command."""
...
{% endif %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Copyright (c) 2024 Snowflake Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from snowflake.cli.api.plugins.command import plugin_hook_impl
from snowflake.cli.api.plugins.command.bridge import build_command_spec

from {{ cookiecutter.package_namespace }}.{{ cookiecutter.plugin_module }}.handler import (
{{ cookiecutter.plugin_module | replace('_', ' ') | title | replace(' ', '') }}HandlerImpl,
)
from {{ cookiecutter.package_namespace }}.{{ cookiecutter.plugin_module }}.interface import PLUGIN_SPEC


@plugin_hook_impl
def command_spec():
return build_command_spec(PLUGIN_SPEC, {{ cookiecutter.plugin_module | replace('_', ' ') | title | replace(' ', '') }}HandlerImpl())
13 changes: 13 additions & 0 deletions plugin-template/{{cookiecutter.plugin_name}}/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright (c) 2024 Snowflake Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
Loading
Loading