Skip to content
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

LSP autogenerated contracts POC, DO NOT MERGE #1623

Draft
wants to merge 8 commits into
base: nade-dex-lsp
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
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
22 changes: 22 additions & 0 deletions lsp_api_contracts/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
generate/lsp_api_python_models:
# https://openapi-generator.tech/docs/generators/python
# --global-property models, generates only models, see https://github.com/OpenAPITools/openapi-generator/issues/12754#issuecomment-1282285724
docker run --rm -v "${PWD}:/lsp_api_contracts" \
-v "${PWD}/../src:/src" \
openapitools/openapi-generator-cli generate \
--global-property models \
-p packageName=snowflake.cli.plugins.lsp \
-i /lsp_api_contracts/lsp_api_contracts.yaml \
-g python \
-o /src

generate/lsp_api_ts_node_models:
# https://openapi-generator.tech/docs/generators/typescript-node
docker run --rm -v "${PWD}:/lsp_api_contracts" openapitools/openapi-generator-cli generate \
--global-property models \
-p modelPackage=lsp_api_contracts \
-i /lsp_api_contracts/lsp_api_contracts.yaml \
-g typescript-node \
-o /lsp_api_contracts/auto_generated/ts

generate: generate/lsp_api_python_models generate/lsp_api_ts_node_models
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* LSP Contracts
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 1.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/

import { RequestFile } from './models';

export class CmdPocInput {
'input1'?: string;
'input2'?: string;

static discriminator: string | undefined = undefined;

static attributeTypeMap: Array<{name: string, baseName: string, type: string}> = [
{
"name": "input1",
"baseName": "input_1",
"type": "string"
},
{
"name": "input2",
"baseName": "input_2",
"type": "string"
} ];

static getAttributeTypeMap() {
return CmdPocInput.attributeTypeMap;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* LSP Contracts
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 1.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/

import { RequestFile } from './models';

export class CmdPocOutput {
'output1'?: string;
'output2'?: string;

static discriminator: string | undefined = undefined;

static attributeTypeMap: Array<{name: string, baseName: string, type: string}> = [
{
"name": "output1",
"baseName": "output_1",
"type": "string"
},
{
"name": "output2",
"baseName": "output_2",
"type": "string"
} ];

static getAttributeTypeMap() {
return CmdPocOutput.attributeTypeMap;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* LSP Contracts
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 1.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/

import { RequestFile } from './models';

export class Connection {
'account'?: string;
'user'?: string;

static discriminator: string | undefined = undefined;

static attributeTypeMap: Array<{name: string, baseName: string, type: string}> = [
{
"name": "account",
"baseName": "account",
"type": "string"
},
{
"name": "user",
"baseName": "user",
"type": "string"
} ];

static getAttributeTypeMap() {
return Connection.attributeTypeMap;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* LSP Contracts
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 1.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/

import { RequestFile } from './models';
import { Connection } from './connection';

export class Context {
'connection'?: Connection;
'env'?: { [key: string]: string; };
'projectPath'?: string;

static discriminator: string | undefined = undefined;

static attributeTypeMap: Array<{name: string, baseName: string, type: string}> = [
{
"name": "connection",
"baseName": "connection",
"type": "Connection"
},
{
"name": "env",
"baseName": "env",
"type": "{ [key: string]: string; }"
},
{
"name": "projectPath",
"baseName": "project_path",
"type": "string"
} ];

static getAttributeTypeMap() {
return Context.attributeTypeMap;
}
}

47 changes: 47 additions & 0 deletions lsp_api_contracts/lsp_api_contracts.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
openapi: 3.0.0
info:
title: LSP Contracts
version: 1.0.0
paths:
/not_an_api:
post:
summary: Mock api to allow openapi-generator-cli to work with this file
responses:
'500':
description: not a real implementation
components:
schemas:
Connection:
type: object
properties:
account:
type: string
user:
type: string
EnvOverrides:
type: object
additionalProperties:
type: string
Context:
type: object
properties:
connection:
$ref: '#/components/schemas/Connection'
env:
$ref: '#/components/schemas/EnvOverrides'
project_path:
type: string
CmdPocInput:
type: object
properties:
input_1:
type: string
input_2:
type: string
CmdPocOutput:
type: object
properties:
output_1:
type: string
output_2:
type: string
12 changes: 10 additions & 2 deletions src/snowflake/cli/_plugins/nativeapp/lsp_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
from snowflake.cli.api.cli_global_context import get_cli_context
from snowflake.cli.api.output.types import MessageResult
from snowflake.cli.api.project.definition_manager import DefinitionManager
from snowflake.cli.plugins.lsp.models.cmd_poc_input import CmdPocInput
from snowflake.cli.plugins.lsp.models.cmd_poc_output import CmdPocOutput
from snowflake.cli.plugins.lsp.server import lsp_plugin
from snowflake.cli.plugins.nativeapp.manager import NativeAppManagern
from snowflake.cli._plugins.nativeapp.manager import NativeAppManager


@lsp_plugin(
Expand All @@ -42,7 +44,6 @@ def open_app() -> MessageResult:
manager = NativeAppManager(
project_definition=project_definition,
project_root=project_root,
connection=ctx.connection,
)
if manager.get_existing_app_info():
url = manager.get_snowsight_url()
Expand All @@ -51,3 +52,10 @@ def open_app() -> MessageResult:
return MessageResult(
'Snowflake Native App not yet deployed! Please run "runApplication" first.'
)

@workspace_command(server, "poc_cmd")
def poc_cmd(input: CmdPocInput) -> CmdPocOutput:
return CmdPocOutput(
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the default type checking behavior in pycharm and visual studio DOES NOT seem to complain about type violations (the whole point of this).

I got it to behave as expected in pycharm after installing the pydantic plugin. To make sure bugs are detected, we would want to stricter type validation (if not there yet during "build" time) using mypy or equivalent.

Screenshot 2024-09-25 at 7 33 28 PM

output_1=input.input_1,
output_2=input.input_2,
)
54 changes: 31 additions & 23 deletions src/snowflake/cli/plugins/lsp/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,11 @@
import inspect
from typing import Any, Callable, Dict, List, Optional, Union, get_type_hints, is_typeddict
from pydantic import TypeAdapter, ValidationError
from typing_extensions import TypedDict, NotRequired
from snowflake.cli.api.cli_global_context import CliContextArguments, fork_cli_context

from pygls.server import LanguageServer


ORIGINAL_FUNCTION_KEY = "__lsp_original_function__"


TypeDef = Dict[str, Union[str, 'TypeDef']]

class CommandArguments(CliContextArguments):
"""
The arguments that can be passed to a workspace command.

"""
args: NotRequired[List[Any]]
kwargs: NotRequired[Dict[str, Any]]
from snowflake.cli.plugins.snowflake.cli.plugins.lsp.models.context import Context


def workspace_command(
Expand All @@ -47,30 +34,51 @@ def workspace_command(
(if required) as well as ensuring arguments are in the required format.
"""
def _decorator(func):
"""
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Retrieve type hints in the method

validate once during load time that func
- contains 0 or 1 argument
- must be typed and contain a "from_dict" method
"""
sig = inspect.signature(func)
func_params = list(sig.parameters.values())
if len(func_params) > 1:
raise TypeError("func is not expected to have more than 1 argument")

input_cmd_model_type = None
if len(func_params) == 1:
input_cmd_model_type = func_params[0].annotation
if not hasattr(input_cmd_model_type, "from_dict"):
raise TypeError("func argument is expected to have a from_dict method")

@server.command(name)
def wrapper(params: List[CommandArguments]):
if len(params) > 1:
def wrapper(arguments: List[Dict]):
if len(arguments) > 1:
raise ValueError("Expected exactly one CommandArguments object")

try:
args = TypeAdapter(CommandArguments).validate_python(params[0] if len(params) == 1 else {})
lsp_raw_payload = arguments[0]
context = Context.from_dict(lsp_raw_payload["context"])

if requires_connection and "connection" not in args:
if requires_connection and not context.connection:
raise ValueError("connection missing, but requires_connection=True")

if requires_project and "project_path" not in args:
if requires_project and not context.project_path:
raise ValueError("project_path missing, but requires_connection=True")

# TODO: validation of args.args / args.kwargs based on shape of actual command...

with fork_cli_context(**args):
return func(*args.get("args", []), **args.get("kwargs", {}))
with fork_cli_context(
connection_overrides=context.connection,
project_env_overrides=context.env,
project_path=context.project_path,
):
if input_cmd_model_type:
return func(input_cmd_model_type.from_dict(lsp_raw_payload["cmd"]))
return func()

except ValidationError as exc:
raise ValueError(f"ERROR: Invalid schema: {exc}")


setattr(wrapper, ORIGINAL_FUNCTION_KEY, func)
return wrapper

return _decorator
Loading
Loading