Skip to content

Commit

Permalink
feat: install applications via plugins (#275)
Browse files Browse the repository at this point in the history
  • Loading branch information
jamagalhaes authored Jan 7, 2025
1 parent 84ecfe9 commit d0a5f59
Show file tree
Hide file tree
Showing 24 changed files with 275 additions and 50 deletions.
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."""
pass
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

0 comments on commit d0a5f59

Please sign in to comment.