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

feat: install applications via plugins #275

Merged
merged 8 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from 7 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
17 changes: 13 additions & 4 deletions canvas_cli/apps/plugin/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,14 +143,23 @@ def _get_protocols_with_new_cqm_properties(
return protocol_props if has_updates else None


def get_base_plugin_template_path() -> Path:
def get_base_plugin_template_path(plugin_type: str) -> Path:
"""Return context's base_plugin_template_path, so it can be used as a Typer default."""
return context.plugin_template_dir / context.default_plugin_template_name
match plugin_type:
case "application":
return context.plugin_template_dir / "application"
case _:
return context.plugin_template_dir / context.default_plugin_template_name


def init() -> None:
def init(
plugin_type: str = typer.Argument(
"protocol",
help="The type of plugin to create. Options are 'application' or 'protocol'.",
),
) -> None:
"""Create a new plugin."""
template = get_base_plugin_template_path()
template = get_base_plugin_template_path(plugin_type)
try:
project_dir = cookiecutter(str(template))
except OutputDirExistsException:
Expand Down
4 changes: 4 additions & 0 deletions canvas_cli/templates/plugins/application/cookiecutter.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"project_name": "My Cool Application",
"__project_slug": "{{ cookiecutter.project_name.lower().replace(' ', '_') }}"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"sdk_version": "0.1.4",
"plugin_version": "0.0.1",
"name": "{{ cookiecutter.__project_slug }}",
"description": "Edit the description in CANVAS_MANIFEST.json",
"components": {
"applications": [
{
"class": "{{ cookiecutter.__project_slug }}.applications.my_application:MyApplication",
"name": "My Application",
"description": "An Application that does xyz...",
"scope": "global",
"icon": "assets/python-logo.png",
"origins": []
}
],
"commands": [],
"content": [],
"effects": [],
"views": []
},
"secrets": [],
"tags": {},
"references": [],
"license": "",
"diagram": false,
"readme": "./README.md"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{{ cookiecutter.__project_slug }}
{% for _ in cookiecutter.__project_slug %}={% endfor %}

## Description

A description of this plugin

### Important Note!

The CANVAS_MANIFEST.json is used when installing your plugin. Please ensure it
gets updated if you add, remove, or rename protocols.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from canvas_sdk.effects import Effect
from canvas_sdk.effects.launch_modal import LaunchModalEffect
from canvas_sdk.handlers.application import Application


class MyApplication(Application):
"""An embeddable application that can be registered to Canvas."""

def on_open(self) -> Effect:
"""Handle the on_open event."""
# Implement this method to handle the application on_open event.
return LaunchModalEffect(url="", target=LaunchModalEffect.TargetType.DEFAULT_MODAL).apply()
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 18 additions & 1 deletion canvas_cli/utils/validators/manifest_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"content": {"$ref": "#/$defs/component"},
"effects": {"$ref": "#/$defs/component"},
"views": {"$ref": "#/$defs/component"},
"applications": {"$ref": "#/$defs/applications"},
},
"additionalProperties": False,
"minProperties": 1,
Expand Down Expand Up @@ -76,6 +77,22 @@
"required": ["class", "description", "data_access"],
"additionalProperties": False,
},
}
},
"applications": {
"type": "array",
"items": {
"type": "object",
"properties": {
"class": {"type": "string"},
"name": {"type": "string", "maxLength": 32},
"description": {"type": "string", "maxLength": 256},
"icon": {"type": "string"},
"scope": {"type": "string", "enum": ["patient_specific", "global"]},
"origins": {"type": "array", "items": {"type": "string"}},
},
"required": ["class", "icon", "scope", "name", "description", "origins"],
"additionalProperties": False,
},
},
},
}
4 changes: 2 additions & 2 deletions canvas_generated/messages/effects_pb2.py

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions canvas_generated/messages/effects_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ class EffectType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
ENTER_IN_ERROR_STRUCTURED_ASSESSMENT_COMMAND: _ClassVar[EffectType]
SHOW_ACTION_BUTTON: _ClassVar[EffectType]
PATIENT_PORTAL__INTAKE_FORM_RESULTS: _ClassVar[EffectType]
LAUNCH_MODAL: _ClassVar[EffectType]
UNKNOWN_EFFECT: EffectType
LOG: EffectType
ADD_PLAN_COMMAND: EffectType
Expand Down Expand Up @@ -317,6 +318,7 @@ COMMIT_STRUCTURED_ASSESSMENT_COMMAND: EffectType
ENTER_IN_ERROR_STRUCTURED_ASSESSMENT_COMMAND: EffectType
SHOW_ACTION_BUTTON: EffectType
PATIENT_PORTAL__INTAKE_FORM_RESULTS: EffectType
LAUNCH_MODAL: EffectType

class Effect(_message.Message):
__slots__ = ("type", "payload", "plugin_name", "classname")
Expand Down
4 changes: 2 additions & 2 deletions canvas_generated/messages/events_pb2.py

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions canvas_generated/messages/events_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,7 @@ class EventType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
CLAIM__CONDITIONS: _ClassVar[EventType]
PLUGIN_CREATED: _ClassVar[EventType]
PLUGIN_UPDATED: _ClassVar[EventType]
APPLICATION__ON_OPEN: _ClassVar[EventType]
PATIENT_PORTAL__GET_INTAKE_FORMS: _ClassVar[EventType]
UNKNOWN: EventType
ALLERGY_INTOLERANCE_CREATED: EventType
Expand Down Expand Up @@ -1306,6 +1307,7 @@ PATIENT_PROFILE__ADD_PHARMACY__POST_SEARCH: EventType
CLAIM__CONDITIONS: EventType
PLUGIN_CREATED: EventType
PLUGIN_UPDATED: EventType
APPLICATION__ON_OPEN: EventType
PATIENT_PORTAL__GET_INTAKE_FORMS: EventType

class Event(_message.Message):
Expand Down
4 changes: 3 additions & 1 deletion canvas_sdk/effects/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from canvas_generated.messages.effects_pb2 import Effect, EffectType

__all__ = ("Effect", "EffectType")
from .base import _BaseEffect

__all__ = ("Effect", "EffectType", "_BaseEffect")
24 changes: 24 additions & 0 deletions canvas_sdk/effects/launch_modal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from enum import StrEnum
from typing import Any

from canvas_sdk.effects import EffectType, _BaseEffect


class LaunchModalEffect(_BaseEffect):
"""An Effect that will launch a modal."""

class Meta:
effect_type = EffectType.LAUNCH_MODAL

class TargetType(StrEnum):
DEFAULT_MODAL = "default_modal"
NEW_WINDOW = "new_window"
RIGHT_CHART_PANE = "right_chart_pane"

url: str
target: TargetType = TargetType.DEFAULT_MODAL

@property
def values(self) -> dict[str, Any]:
"""The LaunchModalEffect values."""
return {"url": self.url, "target": self.target.value}
29 changes: 29 additions & 0 deletions canvas_sdk/handlers/application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from abc import ABC, abstractmethod

from canvas_sdk.effects import Effect
from canvas_sdk.events import EventType
from canvas_sdk.handlers import BaseHandler


class Application(BaseHandler, ABC):
"""An embeddable application that can be registered to Canvas."""

RESPONDS_TO = [EventType.Name(EventType.APPLICATION__ON_OPEN)]

def compute(self) -> list[Effect]:
"""Handle the application events."""
match self.event.type:
case EventType.APPLICATION__ON_OPEN:
return [self.on_open()] if self.target == self.identifier else []
case _:
return []

@abstractmethod
def on_open(self) -> Effect:
"""Handle the application open event."""
...

@property
def identifier(self) -> str:
"""The application identifier."""
return f"{self.__class__.__module__}:{self.__class__.__qualname__}"
12 changes: 8 additions & 4 deletions canvas_sdk/handlers/base.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import importlib.metadata
from abc import ABC
from abc import ABC, abstractmethod
from typing import Any

import deprecation

from canvas_sdk.effects import Effect
from canvas_sdk.events import Event

version = importlib.metadata.version("canvas")


class BaseHandler(ABC):
"""
The class that all handlers inherit from.
"""
"""The class that all handlers inherit from."""

secrets: dict[str, Any]
event: Event
Expand Down Expand Up @@ -46,3 +45,8 @@ def context(self) -> dict[str, Any]:
def target(self) -> str:
"""The target id of the event."""
return self.event.target.id

@abstractmethod
def compute(self) -> list[Effect]:
"""Compute the effects to be applied."""
...
jamagalhaes marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 3 additions & 1 deletion canvas_sdk/protocols/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from abc import ABC

from canvas_sdk.handlers.base import BaseHandler


class BaseProtocol(BaseHandler):
class BaseProtocol(BaseHandler, ABC):
"""
The class that protocols inherit from.
"""
Expand Down
Loading
Loading