From caab056024a1e7c3eeac91c0f425b07e529b04a7 Mon Sep 17 00:00:00 2001 From: Beau Gunderson Date: Fri, 15 Mar 2024 10:46:51 -0600 Subject: [PATCH 01/13] WIP for plugin reloading --- .../generated/messages/plugins_pb2.py | 28 +++++++++++++++ .../generated/messages/plugins_pb2.pyi | 15 ++++++++ .../generated/messages/plugins_pb2_grpc.py | 4 +++ .../generated/services/plugin_runner_pb2.py | 7 ++-- .../generated/services/plugin_runner_pb2.pyi | 1 + .../services/plugin_runner_pb2_grpc.py | 34 +++++++++++++++++++ plugin_runner/plugin_runner.py | 32 ++++++----------- protobufs/generate_protobufs.sh | 15 ++++++-- protobufs/generated/messages/plugins.proto | 9 +++++ .../generated/services/plugin_runner.proto | 3 ++ 10 files changed, 121 insertions(+), 27 deletions(-) create mode 100644 plugin_runner/generated/messages/plugins_pb2.py create mode 100644 plugin_runner/generated/messages/plugins_pb2.pyi create mode 100644 plugin_runner/generated/messages/plugins_pb2_grpc.py create mode 100644 protobufs/generated/messages/plugins.proto diff --git a/plugin_runner/generated/messages/plugins_pb2.py b/plugin_runner/generated/messages/plugins_pb2.py new file mode 100644 index 00000000..0ffc2174 --- /dev/null +++ b/plugin_runner/generated/messages/plugins_pb2.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: generated/messages/plugins.proto +# Protobuf Python Version: 4.25.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n generated/messages/plugins.proto\"\x16\n\x14ReloadPluginsRequest\"(\n\x15ReloadPluginsResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'generated.messages.plugins_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_RELOADPLUGINSREQUEST']._serialized_start=36 + _globals['_RELOADPLUGINSREQUEST']._serialized_end=58 + _globals['_RELOADPLUGINSRESPONSE']._serialized_start=60 + _globals['_RELOADPLUGINSRESPONSE']._serialized_end=100 +# @@protoc_insertion_point(module_scope) diff --git a/plugin_runner/generated/messages/plugins_pb2.pyi b/plugin_runner/generated/messages/plugins_pb2.pyi new file mode 100644 index 00000000..2af3ce2a --- /dev/null +++ b/plugin_runner/generated/messages/plugins_pb2.pyi @@ -0,0 +1,15 @@ +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional + +DESCRIPTOR: _descriptor.FileDescriptor + +class ReloadPluginsRequest(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class ReloadPluginsResponse(_message.Message): + __slots__ = ("success",) + SUCCESS_FIELD_NUMBER: _ClassVar[int] + success: bool + def __init__(self, success: bool = ...) -> None: ... diff --git a/plugin_runner/generated/messages/plugins_pb2_grpc.py b/plugin_runner/generated/messages/plugins_pb2_grpc.py new file mode 100644 index 00000000..2daafffe --- /dev/null +++ b/plugin_runner/generated/messages/plugins_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/plugin_runner/generated/services/plugin_runner_pb2.py b/plugin_runner/generated/services/plugin_runner_pb2.py index 15e5acd6..3763dcff 100644 --- a/plugin_runner/generated/services/plugin_runner_pb2.py +++ b/plugin_runner/generated/services/plugin_runner_pb2.py @@ -13,15 +13,16 @@ from generated.messages import events_pb2 as generated_dot_messages_dot_events__pb2 +from generated.messages import plugins_pb2 as generated_dot_messages_dot_plugins__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n&generated/services/plugin_runner.proto\x12\x06\x63\x61nvas\x1a\x1fgenerated/messages/events.proto2E\n\x0cPluginRunner\x12\x35\n\x0bHandleEvent\x12\r.canvas.Event\x1a\x15.canvas.EventResponse0\x01\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n&generated/services/plugin_runner.proto\x12\x06\x63\x61nvas\x1a\x1fgenerated/messages/events.proto\x1a generated/messages/plugins.proto2\x87\x01\n\x0cPluginRunner\x12\x35\n\x0bHandleEvent\x12\r.canvas.Event\x1a\x15.canvas.EventResponse0\x01\x12@\n\rReloadPlugins\x12\x15.ReloadPluginsRequest\x1a\x16.ReloadPluginsResponse0\x01\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'generated.services.plugin_runner_pb2', _globals) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None - _globals['_PLUGINRUNNER']._serialized_start=83 - _globals['_PLUGINRUNNER']._serialized_end=152 + _globals['_PLUGINRUNNER']._serialized_start=118 + _globals['_PLUGINRUNNER']._serialized_end=253 # @@protoc_insertion_point(module_scope) diff --git a/plugin_runner/generated/services/plugin_runner_pb2.pyi b/plugin_runner/generated/services/plugin_runner_pb2.pyi index e4f93da2..c230dcd1 100644 --- a/plugin_runner/generated/services/plugin_runner_pb2.pyi +++ b/plugin_runner/generated/services/plugin_runner_pb2.pyi @@ -1,4 +1,5 @@ from generated.messages import events_pb2 as _events_pb2 +from generated.messages import plugins_pb2 as _plugins_pb2 from google.protobuf import descriptor as _descriptor from typing import ClassVar as _ClassVar diff --git a/plugin_runner/generated/services/plugin_runner_pb2_grpc.py b/plugin_runner/generated/services/plugin_runner_pb2_grpc.py index 83f2b63e..2ce3f3ce 100644 --- a/plugin_runner/generated/services/plugin_runner_pb2_grpc.py +++ b/plugin_runner/generated/services/plugin_runner_pb2_grpc.py @@ -3,6 +3,7 @@ import grpc from generated.messages import events_pb2 as generated_dot_messages_dot_events__pb2 +from generated.messages import plugins_pb2 as generated_dot_messages_dot_plugins__pb2 class PluginRunnerStub(object): @@ -19,6 +20,11 @@ def __init__(self, channel): request_serializer=generated_dot_messages_dot_events__pb2.Event.SerializeToString, response_deserializer=generated_dot_messages_dot_events__pb2.EventResponse.FromString, ) + self.ReloadPlugins = channel.unary_stream( + '/canvas.PluginRunner/ReloadPlugins', + request_serializer=generated_dot_messages_dot_plugins__pb2.ReloadPluginsRequest.SerializeToString, + response_deserializer=generated_dot_messages_dot_plugins__pb2.ReloadPluginsResponse.FromString, + ) class PluginRunnerServicer(object): @@ -30,6 +36,12 @@ def HandleEvent(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def ReloadPlugins(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def add_PluginRunnerServicer_to_server(servicer, server): rpc_method_handlers = { @@ -38,6 +50,11 @@ def add_PluginRunnerServicer_to_server(servicer, server): request_deserializer=generated_dot_messages_dot_events__pb2.Event.FromString, response_serializer=generated_dot_messages_dot_events__pb2.EventResponse.SerializeToString, ), + 'ReloadPlugins': grpc.unary_stream_rpc_method_handler( + servicer.ReloadPlugins, + request_deserializer=generated_dot_messages_dot_plugins__pb2.ReloadPluginsRequest.FromString, + response_serializer=generated_dot_messages_dot_plugins__pb2.ReloadPluginsResponse.SerializeToString, + ), } generic_handler = grpc.method_handlers_generic_handler( 'canvas.PluginRunner', rpc_method_handlers) @@ -64,3 +81,20 @@ def HandleEvent(request, generated_dot_messages_dot_events__pb2.EventResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def ReloadPlugins(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_stream(request, target, '/canvas.PluginRunner/ReloadPlugins', + generated_dot_messages_dot_plugins__pb2.ReloadPluginsRequest.SerializeToString, + generated_dot_messages_dot_plugins__pb2.ReloadPluginsResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/plugin_runner/plugin_runner.py b/plugin_runner/plugin_runner.py index c438a25e..c77d1e82 100644 --- a/plugin_runner/plugin_runner.py +++ b/plugin_runner/plugin_runner.py @@ -7,32 +7,22 @@ from generated.messages.effects_pb2 import Effect, EffectType from generated.messages.events_pb2 import EventResponse, EventType -from generated.services.plugin_runner_pb2_grpc import PluginRunnerServicer, add_PluginRunnerServicer_to_server +from generated.services.plugin_runner_pb2_grpc import ( + PluginRunnerServicer, + add_PluginRunnerServicer_to_server, +) class PluginRunner(PluginRunnerServicer): async def HandleEvent(self, request, context): event_name = EventType.Name(request.type) - logging.info(f'Handling event: {event_name}') - - effect_type = EffectType.LOG - effect_payload = f'Handled Event: "{event_name}"' - - if event_name == "ASSESS_COMMAND__CONDITION_SELECTED": - received_message = json.loads(request.target) - print(received_message) - has_hypertension_condition = [i for i in received_message["data"]["condition"]["codings"] if i["code"] == "I10" and i["system"] == "ICD-10"] - if has_hypertension_condition: - effect_type = EffectType.ADD_PLAN_COMMAND - effect_dict = { - "note": received_message["note"], - "data": { - "narrative": "Instruct patient to monitor and record blood pressure at home." - } - } - effect_payload = json.dumps(effect_dict) - - yield EventResponse(success=True, effects=[Effect(type=effect_type, payload=effect_payload)]) + + logging.info(f"Handling event: {event_name}") + + yield EventResponse( + success=True, effects=[Effect(type="Log", payload=f'Handled Event: "{event_name}"')] + ) + async def serve(): port = "50051" diff --git a/protobufs/generate_protobufs.sh b/protobufs/generate_protobufs.sh index 49aa7e74..35023e02 100755 --- a/protobufs/generate_protobufs.sh +++ b/protobufs/generate_protobufs.sh @@ -1,4 +1,13 @@ -#! /bin/sh +#!/usr/bin/env bash -# poetry run python -m grpc_tools.protoc -I=protobufs/ --python_out=plugin_runner/ --pyi_out=plugin_runner/ --grpc_python_out=plugin_runner/ protobufs/generated/**/*.proto -python -m grpc_tools.protoc -I=protobufs/ --python_out=plugin_runner/ --pyi_out=plugin_runner/ --grpc_python_out=plugin_runner/ protobufs/generated/**/*.proto +pushd .. + +python \ + -m grpc_tools.protoc \ + -I=protobufs/ \ + --python_out=plugin_runner/ \ + --pyi_out=plugin_runner/ \ + --grpc_python_out=plugin_runner/ \ + protobufs/generated/**/*.proto + +popd diff --git a/protobufs/generated/messages/plugins.proto b/protobufs/generated/messages/plugins.proto new file mode 100644 index 00000000..e1863659 --- /dev/null +++ b/protobufs/generated/messages/plugins.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +message ReloadPluginsRequest { + +} + +message ReloadPluginsResponse { + bool success = 1; +} diff --git a/protobufs/generated/services/plugin_runner.proto b/protobufs/generated/services/plugin_runner.proto index a9bda501..e31a7b22 100644 --- a/protobufs/generated/services/plugin_runner.proto +++ b/protobufs/generated/services/plugin_runner.proto @@ -1,9 +1,12 @@ syntax = 'proto3'; import "generated/messages/events.proto"; +import "generated/messages/plugins.proto"; package canvas; service PluginRunner { rpc HandleEvent (Event) returns (stream EventResponse); + + rpc ReloadPlugins (ReloadPluginsRequest) returns (stream ReloadPluginsResponse); } From ea2d42a0fbe1b7cafdb9478086a1521b35481b8e Mon Sep 17 00:00:00 2001 From: Beau Gunderson Date: Fri, 15 Mar 2024 10:51:21 -0600 Subject: [PATCH 02/13] add simple module reloading --- plugin_runner/plugin_runner.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/plugin_runner/plugin_runner.py b/plugin_runner/plugin_runner.py index c77d1e82..53ee525e 100644 --- a/plugin_runner/plugin_runner.py +++ b/plugin_runner/plugin_runner.py @@ -1,20 +1,26 @@ import asyncio import json +import importlib import logging import grpc from generated.messages.effects_pb2 import Effect, EffectType from generated.messages.events_pb2 import EventResponse, EventType +from generated.messages.events_pb2 import Event, EventResponse, EventType +from generated.messages.plugins_pb2 import ReloadPluginsRequest, ReloadPluginsResponse from generated.services.plugin_runner_pb2_grpc import ( PluginRunnerServicer, add_PluginRunnerServicer_to_server, ) +# TODO load and store plugins globally +LOADED_PLUGINS = {} + class PluginRunner(PluginRunnerServicer): - async def HandleEvent(self, request, context): + async def HandleEvent(self, request: Event, context): event_name = EventType.Name(request.type) logging.info(f"Handling event: {event_name}") @@ -23,6 +29,16 @@ async def HandleEvent(self, request, context): success=True, effects=[Effect(type="Log", payload=f'Handled Event: "{event_name}"')] ) + async def ReloadPlugins(self, request: ReloadPluginsRequest, context): + try: + for name, module in LOADED_PLUGINS.items(): + logging.info(f"Reloading plugin: {name}") + importlib.reload(module) + except ImportError: + yield ReloadPluginsResponse(success=False) + else: + yield ReloadPluginsResponse(success=True) + async def serve(): port = "50051" From 216935c84e714018ba5092969a99b680f8074c96 Mon Sep 17 00:00:00 2001 From: Beau Gunderson Date: Fri, 15 Mar 2024 10:58:21 -0600 Subject: [PATCH 03/13] more WIP --- plugin_runner/plugin_runner.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/plugin_runner/plugin_runner.py b/plugin_runner/plugin_runner.py index 53ee525e..3d218817 100644 --- a/plugin_runner/plugin_runner.py +++ b/plugin_runner/plugin_runner.py @@ -40,12 +40,24 @@ async def ReloadPlugins(self, request: ReloadPluginsRequest, context): yield ReloadPluginsResponse(success=True) +def load_plugins(): + # TODO: walk plugins directory, import and add each plugin to + # LOADED_PLUGINS + pass + + async def serve(): port = "50051" + server = grpc.aio.server() - add_PluginRunnerServicer_to_server(PluginRunner(), server) server.add_insecure_port("127.0.0.1:" + port) - logging.info("Starting server, listening on " + port) + + add_PluginRunnerServicer_to_server(PluginRunner(), server) + + logging.info(f"Starting server, listening on port {port}") + + load_plugins() + await server.start() await server.wait_for_termination() From 82c948fbf59c422c3a6cd6ab527a1b6f8c652949 Mon Sep 17 00:00:00 2001 From: Joe Wilson Date: Wed, 20 Mar 2024 13:05:10 -0700 Subject: [PATCH 04/13] Changes effect type. --- plugin_runner/plugin_runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin_runner/plugin_runner.py b/plugin_runner/plugin_runner.py index 3d218817..c9827d40 100644 --- a/plugin_runner/plugin_runner.py +++ b/plugin_runner/plugin_runner.py @@ -26,7 +26,7 @@ async def HandleEvent(self, request: Event, context): logging.info(f"Handling event: {event_name}") yield EventResponse( - success=True, effects=[Effect(type="Log", payload=f'Handled Event: "{event_name}"')] + success=True, effects=[Effect(type=EffectType.LOG, payload=f'Handled Event: "{event_name}"')] ) async def ReloadPlugins(self, request: ReloadPluginsRequest, context): From 612cacd24c63ad24842f903fd0f51904b57074af Mon Sep 17 00:00:00 2001 From: Joe Wilson Date: Wed, 20 Mar 2024 13:59:35 -0700 Subject: [PATCH 05/13] Fixes plugin structure and installation issues. --- .../{{ cookiecutter.project_slug }}/pyproject.toml | 14 ++++++++++++++ .../commands/__init__.py | 0 .../content/__init__.py | 0 .../effects/__init__.py | 0 .../protocols/__init__.py | 0 .../views/__init__.py | 0 6 files changed, 14 insertions(+) create mode 100644 canvas_cli/templates/plugins/default/{{ cookiecutter.project_slug }}/pyproject.toml rename canvas_cli/templates/plugins/default/{{ cookiecutter.project_slug }}/{ => {{ cookiecutter.project_slug }}}/commands/__init__.py (100%) rename canvas_cli/templates/plugins/default/{{ cookiecutter.project_slug }}/{ => {{ cookiecutter.project_slug }}}/content/__init__.py (100%) rename canvas_cli/templates/plugins/default/{{ cookiecutter.project_slug }}/{ => {{ cookiecutter.project_slug }}}/effects/__init__.py (100%) rename canvas_cli/templates/plugins/default/{{ cookiecutter.project_slug }}/{ => {{ cookiecutter.project_slug }}}/protocols/__init__.py (100%) rename canvas_cli/templates/plugins/default/{{ cookiecutter.project_slug }}/{ => {{ cookiecutter.project_slug }}}/views/__init__.py (100%) diff --git a/canvas_cli/templates/plugins/default/{{ cookiecutter.project_slug }}/pyproject.toml b/canvas_cli/templates/plugins/default/{{ cookiecutter.project_slug }}/pyproject.toml new file mode 100644 index 00000000..168683d0 --- /dev/null +++ b/canvas_cli/templates/plugins/default/{{ cookiecutter.project_slug }}/pyproject.toml @@ -0,0 +1,14 @@ +[tool.poetry] +classifiers = ["Environment :: Canvas :: Plugins"] +name = "{{ cookiecutter.project_name.lower().strip().replace(' ', '_') }}" +version = "{{ cookiecutter.version }}" +description = "" +authors = ["Beau Gunderson "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.11" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/canvas_cli/templates/plugins/default/{{ cookiecutter.project_slug }}/commands/__init__.py b/canvas_cli/templates/plugins/default/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/commands/__init__.py similarity index 100% rename from canvas_cli/templates/plugins/default/{{ cookiecutter.project_slug }}/commands/__init__.py rename to canvas_cli/templates/plugins/default/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/commands/__init__.py diff --git a/canvas_cli/templates/plugins/default/{{ cookiecutter.project_slug }}/content/__init__.py b/canvas_cli/templates/plugins/default/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/content/__init__.py similarity index 100% rename from canvas_cli/templates/plugins/default/{{ cookiecutter.project_slug }}/content/__init__.py rename to canvas_cli/templates/plugins/default/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/content/__init__.py diff --git a/canvas_cli/templates/plugins/default/{{ cookiecutter.project_slug }}/effects/__init__.py b/canvas_cli/templates/plugins/default/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/effects/__init__.py similarity index 100% rename from canvas_cli/templates/plugins/default/{{ cookiecutter.project_slug }}/effects/__init__.py rename to canvas_cli/templates/plugins/default/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/effects/__init__.py diff --git a/canvas_cli/templates/plugins/default/{{ cookiecutter.project_slug }}/protocols/__init__.py b/canvas_cli/templates/plugins/default/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/protocols/__init__.py similarity index 100% rename from canvas_cli/templates/plugins/default/{{ cookiecutter.project_slug }}/protocols/__init__.py rename to canvas_cli/templates/plugins/default/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/protocols/__init__.py diff --git a/canvas_cli/templates/plugins/default/{{ cookiecutter.project_slug }}/views/__init__.py b/canvas_cli/templates/plugins/default/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/views/__init__.py similarity index 100% rename from canvas_cli/templates/plugins/default/{{ cookiecutter.project_slug }}/views/__init__.py rename to canvas_cli/templates/plugins/default/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/views/__init__.py From 33c2e2b71ca181f97f4fbd0e4b12442c35f197e1 Mon Sep 17 00:00:00 2001 From: Joe Wilson Date: Thu, 21 Mar 2024 15:35:46 -0700 Subject: [PATCH 06/13] WIP effects and reloading. --- my_first_plugin/CANVAS_MANIFEST.json | 27 ++++++++++ my_first_plugin/README.md | 18 +++++++ my_first_plugin/__init__.py | 0 .../my_first_plugin/commands/__init__.py | 0 .../my_first_plugin/content/__init__.py | 0 .../my_first_plugin/effects/__init__.py | 0 .../my_first_plugin/protocols/__init__.py | 0 .../my_first_plugin/protocols/protocol.py | 22 ++++++++ .../my_first_plugin/views/__init__.py | 0 my_first_plugin/pyproject.toml | 14 +++++ my_second_plugin/CANVAS_MANIFEST.json | 27 ++++++++++ my_second_plugin/README.md | 18 +++++++ .../my_second_plugin/commands/__init__.py | 0 .../my_second_plugin/content/__init__.py | 0 .../my_second_plugin/effects/__init__.py | 0 .../my_second_plugin/protocols/__init__.py | 0 .../my_second_plugin/views/__init__.py | 0 my_second_plugin/pyproject.toml | 14 +++++ plugin_runner/plugin_runner.py | 52 +++++++++++++++---- 19 files changed, 182 insertions(+), 10 deletions(-) create mode 100644 my_first_plugin/CANVAS_MANIFEST.json create mode 100644 my_first_plugin/README.md create mode 100644 my_first_plugin/__init__.py create mode 100644 my_first_plugin/my_first_plugin/commands/__init__.py create mode 100644 my_first_plugin/my_first_plugin/content/__init__.py create mode 100644 my_first_plugin/my_first_plugin/effects/__init__.py create mode 100644 my_first_plugin/my_first_plugin/protocols/__init__.py create mode 100644 my_first_plugin/my_first_plugin/protocols/protocol.py create mode 100644 my_first_plugin/my_first_plugin/views/__init__.py create mode 100644 my_first_plugin/pyproject.toml create mode 100644 my_second_plugin/CANVAS_MANIFEST.json create mode 100644 my_second_plugin/README.md create mode 100644 my_second_plugin/my_second_plugin/commands/__init__.py create mode 100644 my_second_plugin/my_second_plugin/content/__init__.py create mode 100644 my_second_plugin/my_second_plugin/effects/__init__.py create mode 100644 my_second_plugin/my_second_plugin/protocols/__init__.py create mode 100644 my_second_plugin/my_second_plugin/views/__init__.py create mode 100644 my_second_plugin/pyproject.toml diff --git a/my_first_plugin/CANVAS_MANIFEST.json b/my_first_plugin/CANVAS_MANIFEST.json new file mode 100644 index 00000000..eccc6c5b --- /dev/null +++ b/my_first_plugin/CANVAS_MANIFEST.json @@ -0,0 +1,27 @@ +{ + "sdk_version": "0.1.4", + "plugin_version": "0.1.0", + "name": "My First Plugin", + "description": "A plugin to interact with a Canvas Instance", + "components": { + "protocols": [ + { + "class": "my_first_plugin.protocols.path.to.ProtocolClass", + "description": "A protocol that does xyz...", + "data_access": { + "event": "assess_condition_selected", + "read": [ + "conditions" + ], + "write": [ + "commands" + ] + } + } + ], + "commands": [], + "content": [], + "effects": [], + "views": [] + } +} diff --git a/my_first_plugin/README.md b/my_first_plugin/README.md new file mode 100644 index 00000000..4e6c03b6 --- /dev/null +++ b/my_first_plugin/README.md @@ -0,0 +1,18 @@ +=============== +My First Plugin +=============== + +A plugin to interact with a Canvas Instance + +## Structure + +``` +my_first_plugin/ +├── commands/ +├── content/ +├── effects/ +├── protocols/ +├── views/ +├── CANVAS_MANIFEST.json +└── README.md +``` diff --git a/my_first_plugin/__init__.py b/my_first_plugin/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/my_first_plugin/my_first_plugin/commands/__init__.py b/my_first_plugin/my_first_plugin/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/my_first_plugin/my_first_plugin/content/__init__.py b/my_first_plugin/my_first_plugin/content/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/my_first_plugin/my_first_plugin/effects/__init__.py b/my_first_plugin/my_first_plugin/effects/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/my_first_plugin/my_first_plugin/protocols/__init__.py b/my_first_plugin/my_first_plugin/protocols/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/my_first_plugin/my_first_plugin/protocols/protocol.py b/my_first_plugin/my_first_plugin/protocols/protocol.py new file mode 100644 index 00000000..805311c2 --- /dev/null +++ b/my_first_plugin/my_first_plugin/protocols/protocol.py @@ -0,0 +1,22 @@ +import json + + +class Protocol: + RESPONDS_TO = "ASSESS_COMMAND__CONDITION_SELECTED" + + def __init__(self, event) -> None: + self.event = event + self.payload = json.loads(event.target) + + def compute(self): + return [{ + "effect_type": "ADD_PLAN_COMMAND", + "payload": { + "note": { + "uuid": self.payload["note"]["uuid"] + }, + "data": { + "narrative": "rats" + } + } + }] diff --git a/my_first_plugin/my_first_plugin/views/__init__.py b/my_first_plugin/my_first_plugin/views/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/my_first_plugin/pyproject.toml b/my_first_plugin/pyproject.toml new file mode 100644 index 00000000..c632d7c3 --- /dev/null +++ b/my_first_plugin/pyproject.toml @@ -0,0 +1,14 @@ +[tool.poetry] +classifiers = ["Environment :: Canvas :: Plugins"] +name = "my_first_plugin" +version = "0.1.0" +description = "" +authors = ["Beau Gunderson "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.11" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/my_second_plugin/CANVAS_MANIFEST.json b/my_second_plugin/CANVAS_MANIFEST.json new file mode 100644 index 00000000..4be1557f --- /dev/null +++ b/my_second_plugin/CANVAS_MANIFEST.json @@ -0,0 +1,27 @@ +{ + "sdk_version": "0.1.4", + "plugin_version": "0.1.0", + "name": "My Second Plugin", + "description": "A plugin to interact with a Canvas Instance", + "components": { + "protocols": [ + { + "class": "my_second_plugin.protocols.path.to.ProtocolClass", + "description": "A protocol that does xyz...", + "data_access": { + "event": "assess_condition_selected", + "read": [ + "conditions" + ], + "write": [ + "commands" + ] + } + } + ], + "commands": [], + "content": [], + "effects": [], + "views": [] + } +} diff --git a/my_second_plugin/README.md b/my_second_plugin/README.md new file mode 100644 index 00000000..5df3e985 --- /dev/null +++ b/my_second_plugin/README.md @@ -0,0 +1,18 @@ +================ +My Second Plugin +================ + +A plugin to interact with a Canvas Instance + +## Structure + +``` +my_second_plugin/ +├── commands/ +├── content/ +├── effects/ +├── protocols/ +├── views/ +├── CANVAS_MANIFEST.json +└── README.md +``` diff --git a/my_second_plugin/my_second_plugin/commands/__init__.py b/my_second_plugin/my_second_plugin/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/my_second_plugin/my_second_plugin/content/__init__.py b/my_second_plugin/my_second_plugin/content/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/my_second_plugin/my_second_plugin/effects/__init__.py b/my_second_plugin/my_second_plugin/effects/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/my_second_plugin/my_second_plugin/protocols/__init__.py b/my_second_plugin/my_second_plugin/protocols/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/my_second_plugin/my_second_plugin/views/__init__.py b/my_second_plugin/my_second_plugin/views/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/my_second_plugin/pyproject.toml b/my_second_plugin/pyproject.toml new file mode 100644 index 00000000..dfa7baf5 --- /dev/null +++ b/my_second_plugin/pyproject.toml @@ -0,0 +1,14 @@ +[tool.poetry] +classifiers = ["Environment :: Canvas :: Plugins"] +name = "my_second_plugin" +version = "0.1.0" +description = "" +authors = ["Beau Gunderson "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.11" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/plugin_runner/plugin_runner.py b/plugin_runner/plugin_runner.py index c9827d40..61f18c9a 100644 --- a/plugin_runner/plugin_runner.py +++ b/plugin_runner/plugin_runner.py @@ -15,35 +15,67 @@ add_PluginRunnerServicer_to_server, ) -# TODO load and store plugins globally -LOADED_PLUGINS = {} +import my_first_plugin +import my_second_plugin + +# TODO load and store plugins externally +def get_loaded_plugins(): + return { + "my_first_plugin": my_first_plugin, + "my_second_plugin": my_second_plugin + } class PluginRunner(PluginRunnerServicer): + EVENT_PROTOCOL_MAP = {} + + def __init__(self) -> None: + load_plugins() + self.refresh_event_type_map() + super().__init__() + async def HandleEvent(self, request: Event, context): event_name = EventType.Name(request.type) + relevant_plugins = self.EVENT_PROTOCOL_MAP.get(event_name, []) - logging.info(f"Handling event: {event_name}") + effect_list = [] + for plugin_name in relevant_plugins: + module = get_loaded_plugins().get(plugin_name) + protocol_class = getattr(module, plugin_name).protocols.protocol.Protocol + effects = protocol_class(request).compute() + effect_list = [Effect(type=EffectType.Value(effect["effect_type"]), payload=json.dumps(effect["payload"])) for effect in effects] yield EventResponse( - success=True, effects=[Effect(type=EffectType.LOG, payload=f'Handled Event: "{event_name}"')] + success=True, effects=effect_list ) async def ReloadPlugins(self, request: ReloadPluginsRequest, context): try: - for name, module in LOADED_PLUGINS.items(): - logging.info(f"Reloading plugin: {name}") - importlib.reload(module) + load_plugins() except ImportError: yield ReloadPluginsResponse(success=False) else: + self.refresh_event_type_map() yield ReloadPluginsResponse(success=True) + def refresh_event_type_map(self): + self.EVENT_PROTOCOL_MAP = {} + for name, module in get_loaded_plugins().items(): + protocol_class = None + try: + protocol_file = importlib.import_module(f"{name}.{name}.protocols.protocol") + protocol_class = protocol_file.Protocol + except ImportError: + continue + + if protocol_class and hasattr(protocol_class, "RESPONDS_TO"): + self.EVENT_PROTOCOL_MAP[protocol_class.RESPONDS_TO] = [name] + def load_plugins(): - # TODO: walk plugins directory, import and add each plugin to - # LOADED_PLUGINS - pass + for name, module in get_loaded_plugins().items(): + logging.info(f"Reloading plugin: {name}") + importlib.reload(module) async def serve(): From d193a4e76fedc247789186901b297bf56f495df7 Mon Sep 17 00:00:00 2001 From: Joe Wilson Date: Fri, 22 Mar 2024 09:04:02 -0700 Subject: [PATCH 07/13] WIP reloading fix. --- __init__.py | 0 my_first_plugin/my_first_plugin/__init__.py | 0 .../my_first_plugin/protocols/protocol.py | 2 +- my_second_plugin/__init__.py | 0 my_second_plugin/my_second_plugin/__init__.py | 0 .../my_second_plugin/protocols/protocol.py | 15 +++++++ plugin_runner/plugin_runner.py | 39 +++++++++++-------- 7 files changed, 38 insertions(+), 18 deletions(-) create mode 100644 __init__.py create mode 100644 my_first_plugin/my_first_plugin/__init__.py create mode 100644 my_second_plugin/__init__.py create mode 100644 my_second_plugin/my_second_plugin/__init__.py create mode 100644 my_second_plugin/my_second_plugin/protocols/protocol.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/my_first_plugin/my_first_plugin/__init__.py b/my_first_plugin/my_first_plugin/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/my_first_plugin/my_first_plugin/protocols/protocol.py b/my_first_plugin/my_first_plugin/protocols/protocol.py index 805311c2..22a41917 100644 --- a/my_first_plugin/my_first_plugin/protocols/protocol.py +++ b/my_first_plugin/my_first_plugin/protocols/protocol.py @@ -16,7 +16,7 @@ def compute(self): "uuid": self.payload["note"]["uuid"] }, "data": { - "narrative": "rats" + "narrative": "monkey" } } }] diff --git a/my_second_plugin/__init__.py b/my_second_plugin/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/my_second_plugin/my_second_plugin/__init__.py b/my_second_plugin/my_second_plugin/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/my_second_plugin/my_second_plugin/protocols/protocol.py b/my_second_plugin/my_second_plugin/protocols/protocol.py new file mode 100644 index 00000000..7cf8145b --- /dev/null +++ b/my_second_plugin/my_second_plugin/protocols/protocol.py @@ -0,0 +1,15 @@ +import json + + +class Protocol: + RESPONDS_TO = "SOMETHING_ELSE" + + def __init__(self, event) -> None: + self.event = event + self.payload = json.loads(event.target) + + def compute(self): + return [{ + "effect_type": "ANOTHER_COMMAND_ADDITION", + "payload": {} + }] diff --git a/plugin_runner/plugin_runner.py b/plugin_runner/plugin_runner.py index 61f18c9a..e7804b20 100644 --- a/plugin_runner/plugin_runner.py +++ b/plugin_runner/plugin_runner.py @@ -15,22 +15,27 @@ add_PluginRunnerServicer_to_server, ) -import my_first_plugin -import my_second_plugin +# import my_first_plugin +# import my_second_plugin + +LOADED_PLUGINS = { + "my_first_plugin": __import__("my_first_plugin.my_first_plugin.protocols.protocol"), + "my_second_plugin": __import__("my_second_plugin.my_second_plugin.protocols.protocol") +} # TODO load and store plugins externally -def get_loaded_plugins(): - return { - "my_first_plugin": my_first_plugin, - "my_second_plugin": my_second_plugin - } +# def get_loaded_plugins(): +# return { +# "my_first_plugin": my_first_plugin, +# "my_second_plugin": my_second_plugin +# } class PluginRunner(PluginRunnerServicer): EVENT_PROTOCOL_MAP = {} def __init__(self) -> None: - load_plugins() + # load_plugins() self.refresh_event_type_map() super().__init__() @@ -40,7 +45,7 @@ async def HandleEvent(self, request: Event, context): effect_list = [] for plugin_name in relevant_plugins: - module = get_loaded_plugins().get(plugin_name) + module = LOADED_PLUGINS.get(plugin_name) protocol_class = getattr(module, plugin_name).protocols.protocol.Protocol effects = protocol_class(request).compute() @@ -51,7 +56,7 @@ async def HandleEvent(self, request: Event, context): async def ReloadPlugins(self, request: ReloadPluginsRequest, context): try: - load_plugins() + reload_plugins() except ImportError: yield ReloadPluginsResponse(success=False) else: @@ -60,11 +65,13 @@ async def ReloadPlugins(self, request: ReloadPluginsRequest, context): def refresh_event_type_map(self): self.EVENT_PROTOCOL_MAP = {} - for name, module in get_loaded_plugins().items(): + for name, module in LOADED_PLUGINS.items(): protocol_class = None try: - protocol_file = importlib.import_module(f"{name}.{name}.protocols.protocol") - protocol_class = protocol_file.Protocol + #protocol_file = importlib.import_module(f"{name}.{name}.protocols.protocol") + # breakpoint() + protocol_file = getattr(module, name) + protocol_class = protocol_file.protocols.protocol.Protocol except ImportError: continue @@ -72,8 +79,8 @@ def refresh_event_type_map(self): self.EVENT_PROTOCOL_MAP[protocol_class.RESPONDS_TO] = [name] -def load_plugins(): - for name, module in get_loaded_plugins().items(): +def reload_plugins(): + for name, module in LOADED_PLUGINS.items(): logging.info(f"Reloading plugin: {name}") importlib.reload(module) @@ -88,8 +95,6 @@ async def serve(): logging.info(f"Starting server, listening on port {port}") - load_plugins() - await server.start() await server.wait_for_termination() From 716989160e61c40dd6ee6f6d4605480a2e2cdbf6 Mon Sep 17 00:00:00 2001 From: Beau Gunderson Date: Fri, 22 Mar 2024 10:12:54 -0600 Subject: [PATCH 08/13] WIP for module reloading --- plugin_runner/plugin_runner.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/plugin_runner/plugin_runner.py b/plugin_runner/plugin_runner.py index e7804b20..e4154e10 100644 --- a/plugin_runner/plugin_runner.py +++ b/plugin_runner/plugin_runner.py @@ -18,9 +18,9 @@ # import my_first_plugin # import my_second_plugin -LOADED_PLUGINS = { +LOADED_PLUGINS = { "my_first_plugin": __import__("my_first_plugin.my_first_plugin.protocols.protocol"), - "my_second_plugin": __import__("my_second_plugin.my_second_plugin.protocols.protocol") + "my_second_plugin": __import__("my_second_plugin.my_second_plugin.protocols.protocol"), } # TODO load and store plugins externally @@ -49,10 +49,14 @@ async def HandleEvent(self, request: Event, context): protocol_class = getattr(module, plugin_name).protocols.protocol.Protocol effects = protocol_class(request).compute() - effect_list = [Effect(type=EffectType.Value(effect["effect_type"]), payload=json.dumps(effect["payload"])) for effect in effects] - yield EventResponse( - success=True, effects=effect_list - ) + effect_list = [ + Effect( + type=EffectType.Value(effect["effect_type"]), + payload=json.dumps(effect["payload"]), + ) + for effect in effects + ] + yield EventResponse(success=True, effects=effect_list) async def ReloadPlugins(self, request: ReloadPluginsRequest, context): try: @@ -68,7 +72,7 @@ def refresh_event_type_map(self): for name, module in LOADED_PLUGINS.items(): protocol_class = None try: - #protocol_file = importlib.import_module(f"{name}.{name}.protocols.protocol") + # protocol_file = importlib.import_module(f"{name}.{name}.protocols.protocol") # breakpoint() protocol_file = getattr(module, name) protocol_class = protocol_file.protocols.protocol.Protocol @@ -82,7 +86,7 @@ def refresh_event_type_map(self): def reload_plugins(): for name, module in LOADED_PLUGINS.items(): logging.info(f"Reloading plugin: {name}") - importlib.reload(module) + LOADED_PLUGINS[name] = importlib.reload(module) async def serve(): From 2eead49300c6877bdee298464d457d7d5f64a016 Mon Sep 17 00:00:00 2001 From: Beau Gunderson Date: Fri, 22 Mar 2024 10:37:31 -0600 Subject: [PATCH 09/13] WIP debugging code --- .../my_first_plugin/protocols/protocol.py | 18 +++++++++--------- .../my_second_plugin/protocols/protocol.py | 7 +++---- plugin_runner/plugin_runner.py | 6 ++++++ 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/my_first_plugin/my_first_plugin/protocols/protocol.py b/my_first_plugin/my_first_plugin/protocols/protocol.py index 22a41917..c58725a3 100644 --- a/my_first_plugin/my_first_plugin/protocols/protocol.py +++ b/my_first_plugin/my_first_plugin/protocols/protocol.py @@ -4,19 +4,19 @@ class Protocol: RESPONDS_TO = "ASSESS_COMMAND__CONDITION_SELECTED" + NARRATIVE_STRING = "monkey" + def __init__(self, event) -> None: self.event = event self.payload = json.loads(event.target) def compute(self): - return [{ - "effect_type": "ADD_PLAN_COMMAND", - "payload": { - "note": { - "uuid": self.payload["note"]["uuid"] + return [ + { + "effect_type": "ADD_PLAN_COMMAND", + "payload": { + "note": {"uuid": self.payload["note"]["uuid"]}, + "data": {"narrative": self.NARRATIVE_STRING}, }, - "data": { - "narrative": "monkey" - } } - }] + ] diff --git a/my_second_plugin/my_second_plugin/protocols/protocol.py b/my_second_plugin/my_second_plugin/protocols/protocol.py index 7cf8145b..34bb6728 100644 --- a/my_second_plugin/my_second_plugin/protocols/protocol.py +++ b/my_second_plugin/my_second_plugin/protocols/protocol.py @@ -4,12 +4,11 @@ class Protocol: RESPONDS_TO = "SOMETHING_ELSE" + NARRATIVE_STRING = "" + def __init__(self, event) -> None: self.event = event self.payload = json.loads(event.target) def compute(self): - return [{ - "effect_type": "ANOTHER_COMMAND_ADDITION", - "payload": {} - }] + return [{"effect_type": "ANOTHER_COMMAND_ADDITION", "payload": {}}] diff --git a/plugin_runner/plugin_runner.py b/plugin_runner/plugin_runner.py index e4154e10..d124da0d 100644 --- a/plugin_runner/plugin_runner.py +++ b/plugin_runner/plugin_runner.py @@ -88,6 +88,10 @@ def reload_plugins(): logging.info(f"Reloading plugin: {name}") LOADED_PLUGINS[name] = importlib.reload(module) + protocol_class = getattr(LOADED_PLUGINS[name], name).protocols.protocol.Protocol + + print(f"DEBUG: {name} {protocol_class.NARRATIVE_STRING}") + async def serve(): port = "50051" @@ -99,6 +103,8 @@ async def serve(): logging.info(f"Starting server, listening on port {port}") + reload_plugins() + await server.start() await server.wait_for_termination() From 8952c948e917bc9b389b52b4eff41090bf8e25b3 Mon Sep 17 00:00:00 2001 From: Beau Gunderson Date: Fri, 22 Mar 2024 10:44:00 -0600 Subject: [PATCH 10/13] WIP --- plugin_runner/plugin_runner.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugin_runner/plugin_runner.py b/plugin_runner/plugin_runner.py index d124da0d..c34d043c 100644 --- a/plugin_runner/plugin_runner.py +++ b/plugin_runner/plugin_runner.py @@ -86,7 +86,10 @@ def refresh_event_type_map(self): def reload_plugins(): for name, module in LOADED_PLUGINS.items(): logging.info(f"Reloading plugin: {name}") - LOADED_PLUGINS[name] = importlib.reload(module) + + protocol_module = getattr(LOADED_PLUGINS[name], name).protocols.protocol + + importlib.reload(protocol_module) protocol_class = getattr(LOADED_PLUGINS[name], name).protocols.protocol.Protocol From 415d704b09ce215a67bb96fe8d20e0cbb96d6b16 Mon Sep 17 00:00:00 2001 From: Beau Gunderson Date: Wed, 27 Mar 2024 14:24:29 -0600 Subject: [PATCH 11/13] move custom plugins for dev --- .../my_first_plugin}/CANVAS_MANIFEST.json | 0 .../my_first_plugin}/README.md | 0 .../my_first_plugin}/__init__.py | 0 .../my_first_plugin}/my_first_plugin/__init__.py | 0 .../my_first_plugin/commands/__init__.py | 0 .../my_first_plugin/content/__init__.py | 0 .../my_first_plugin/effects/__init__.py | 0 .../my_first_plugin/protocols/__init__.py | 0 .../my_first_plugin/protocols/protocol.py | 0 .../my_first_plugin/views/__init__.py | 0 .../my_second_plugin}/CANVAS_MANIFEST.json | 0 .../my_second_plugin}/README.md | 0 .../my_second_plugin}/__init__.py | 0 .../my_second_plugin}/my_second_plugin/__init__.py | 0 .../my_second_plugin/commands/__init__.py | 0 .../my_second_plugin/content/__init__.py | 0 .../my_second_plugin/effects/__init__.py | 0 .../my_second_plugin/protocols/__init__.py | 0 .../my_second_plugin/protocols/protocol.py | 0 .../my_second_plugin/views/__init__.py | 0 my_first_plugin/pyproject.toml | 14 -------------- my_second_plugin/pyproject.toml | 14 -------------- 22 files changed, 28 deletions(-) rename {my_first_plugin => custom-plugins/my_first_plugin}/CANVAS_MANIFEST.json (100%) rename {my_first_plugin => custom-plugins/my_first_plugin}/README.md (100%) rename {my_first_plugin => custom-plugins/my_first_plugin}/__init__.py (100%) rename {my_first_plugin => custom-plugins/my_first_plugin}/my_first_plugin/__init__.py (100%) rename {my_first_plugin => custom-plugins/my_first_plugin}/my_first_plugin/commands/__init__.py (100%) rename {my_first_plugin => custom-plugins/my_first_plugin}/my_first_plugin/content/__init__.py (100%) rename {my_first_plugin => custom-plugins/my_first_plugin}/my_first_plugin/effects/__init__.py (100%) rename {my_first_plugin => custom-plugins/my_first_plugin}/my_first_plugin/protocols/__init__.py (100%) rename {my_first_plugin => custom-plugins/my_first_plugin}/my_first_plugin/protocols/protocol.py (100%) rename {my_first_plugin => custom-plugins/my_first_plugin}/my_first_plugin/views/__init__.py (100%) rename {my_second_plugin => custom-plugins/my_second_plugin}/CANVAS_MANIFEST.json (100%) rename {my_second_plugin => custom-plugins/my_second_plugin}/README.md (100%) rename {my_second_plugin => custom-plugins/my_second_plugin}/__init__.py (100%) rename {my_second_plugin => custom-plugins/my_second_plugin}/my_second_plugin/__init__.py (100%) rename {my_second_plugin => custom-plugins/my_second_plugin}/my_second_plugin/commands/__init__.py (100%) rename {my_second_plugin => custom-plugins/my_second_plugin}/my_second_plugin/content/__init__.py (100%) rename {my_second_plugin => custom-plugins/my_second_plugin}/my_second_plugin/effects/__init__.py (100%) rename {my_second_plugin => custom-plugins/my_second_plugin}/my_second_plugin/protocols/__init__.py (100%) rename {my_second_plugin => custom-plugins/my_second_plugin}/my_second_plugin/protocols/protocol.py (100%) rename {my_second_plugin => custom-plugins/my_second_plugin}/my_second_plugin/views/__init__.py (100%) delete mode 100644 my_first_plugin/pyproject.toml delete mode 100644 my_second_plugin/pyproject.toml diff --git a/my_first_plugin/CANVAS_MANIFEST.json b/custom-plugins/my_first_plugin/CANVAS_MANIFEST.json similarity index 100% rename from my_first_plugin/CANVAS_MANIFEST.json rename to custom-plugins/my_first_plugin/CANVAS_MANIFEST.json diff --git a/my_first_plugin/README.md b/custom-plugins/my_first_plugin/README.md similarity index 100% rename from my_first_plugin/README.md rename to custom-plugins/my_first_plugin/README.md diff --git a/my_first_plugin/__init__.py b/custom-plugins/my_first_plugin/__init__.py similarity index 100% rename from my_first_plugin/__init__.py rename to custom-plugins/my_first_plugin/__init__.py diff --git a/my_first_plugin/my_first_plugin/__init__.py b/custom-plugins/my_first_plugin/my_first_plugin/__init__.py similarity index 100% rename from my_first_plugin/my_first_plugin/__init__.py rename to custom-plugins/my_first_plugin/my_first_plugin/__init__.py diff --git a/my_first_plugin/my_first_plugin/commands/__init__.py b/custom-plugins/my_first_plugin/my_first_plugin/commands/__init__.py similarity index 100% rename from my_first_plugin/my_first_plugin/commands/__init__.py rename to custom-plugins/my_first_plugin/my_first_plugin/commands/__init__.py diff --git a/my_first_plugin/my_first_plugin/content/__init__.py b/custom-plugins/my_first_plugin/my_first_plugin/content/__init__.py similarity index 100% rename from my_first_plugin/my_first_plugin/content/__init__.py rename to custom-plugins/my_first_plugin/my_first_plugin/content/__init__.py diff --git a/my_first_plugin/my_first_plugin/effects/__init__.py b/custom-plugins/my_first_plugin/my_first_plugin/effects/__init__.py similarity index 100% rename from my_first_plugin/my_first_plugin/effects/__init__.py rename to custom-plugins/my_first_plugin/my_first_plugin/effects/__init__.py diff --git a/my_first_plugin/my_first_plugin/protocols/__init__.py b/custom-plugins/my_first_plugin/my_first_plugin/protocols/__init__.py similarity index 100% rename from my_first_plugin/my_first_plugin/protocols/__init__.py rename to custom-plugins/my_first_plugin/my_first_plugin/protocols/__init__.py diff --git a/my_first_plugin/my_first_plugin/protocols/protocol.py b/custom-plugins/my_first_plugin/my_first_plugin/protocols/protocol.py similarity index 100% rename from my_first_plugin/my_first_plugin/protocols/protocol.py rename to custom-plugins/my_first_plugin/my_first_plugin/protocols/protocol.py diff --git a/my_first_plugin/my_first_plugin/views/__init__.py b/custom-plugins/my_first_plugin/my_first_plugin/views/__init__.py similarity index 100% rename from my_first_plugin/my_first_plugin/views/__init__.py rename to custom-plugins/my_first_plugin/my_first_plugin/views/__init__.py diff --git a/my_second_plugin/CANVAS_MANIFEST.json b/custom-plugins/my_second_plugin/CANVAS_MANIFEST.json similarity index 100% rename from my_second_plugin/CANVAS_MANIFEST.json rename to custom-plugins/my_second_plugin/CANVAS_MANIFEST.json diff --git a/my_second_plugin/README.md b/custom-plugins/my_second_plugin/README.md similarity index 100% rename from my_second_plugin/README.md rename to custom-plugins/my_second_plugin/README.md diff --git a/my_second_plugin/__init__.py b/custom-plugins/my_second_plugin/__init__.py similarity index 100% rename from my_second_plugin/__init__.py rename to custom-plugins/my_second_plugin/__init__.py diff --git a/my_second_plugin/my_second_plugin/__init__.py b/custom-plugins/my_second_plugin/my_second_plugin/__init__.py similarity index 100% rename from my_second_plugin/my_second_plugin/__init__.py rename to custom-plugins/my_second_plugin/my_second_plugin/__init__.py diff --git a/my_second_plugin/my_second_plugin/commands/__init__.py b/custom-plugins/my_second_plugin/my_second_plugin/commands/__init__.py similarity index 100% rename from my_second_plugin/my_second_plugin/commands/__init__.py rename to custom-plugins/my_second_plugin/my_second_plugin/commands/__init__.py diff --git a/my_second_plugin/my_second_plugin/content/__init__.py b/custom-plugins/my_second_plugin/my_second_plugin/content/__init__.py similarity index 100% rename from my_second_plugin/my_second_plugin/content/__init__.py rename to custom-plugins/my_second_plugin/my_second_plugin/content/__init__.py diff --git a/my_second_plugin/my_second_plugin/effects/__init__.py b/custom-plugins/my_second_plugin/my_second_plugin/effects/__init__.py similarity index 100% rename from my_second_plugin/my_second_plugin/effects/__init__.py rename to custom-plugins/my_second_plugin/my_second_plugin/effects/__init__.py diff --git a/my_second_plugin/my_second_plugin/protocols/__init__.py b/custom-plugins/my_second_plugin/my_second_plugin/protocols/__init__.py similarity index 100% rename from my_second_plugin/my_second_plugin/protocols/__init__.py rename to custom-plugins/my_second_plugin/my_second_plugin/protocols/__init__.py diff --git a/my_second_plugin/my_second_plugin/protocols/protocol.py b/custom-plugins/my_second_plugin/my_second_plugin/protocols/protocol.py similarity index 100% rename from my_second_plugin/my_second_plugin/protocols/protocol.py rename to custom-plugins/my_second_plugin/my_second_plugin/protocols/protocol.py diff --git a/my_second_plugin/my_second_plugin/views/__init__.py b/custom-plugins/my_second_plugin/my_second_plugin/views/__init__.py similarity index 100% rename from my_second_plugin/my_second_plugin/views/__init__.py rename to custom-plugins/my_second_plugin/my_second_plugin/views/__init__.py diff --git a/my_first_plugin/pyproject.toml b/my_first_plugin/pyproject.toml deleted file mode 100644 index c632d7c3..00000000 --- a/my_first_plugin/pyproject.toml +++ /dev/null @@ -1,14 +0,0 @@ -[tool.poetry] -classifiers = ["Environment :: Canvas :: Plugins"] -name = "my_first_plugin" -version = "0.1.0" -description = "" -authors = ["Beau Gunderson "] -readme = "README.md" - -[tool.poetry.dependencies] -python = "^3.11" - -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" diff --git a/my_second_plugin/pyproject.toml b/my_second_plugin/pyproject.toml deleted file mode 100644 index dfa7baf5..00000000 --- a/my_second_plugin/pyproject.toml +++ /dev/null @@ -1,14 +0,0 @@ -[tool.poetry] -classifiers = ["Environment :: Canvas :: Plugins"] -name = "my_second_plugin" -version = "0.1.0" -description = "" -authors = ["Beau Gunderson "] -readme = "README.md" - -[tool.poetry.dependencies] -python = "^3.11" - -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" From 79218ec1047aedacfedff78252a9260a2bb37ab3 Mon Sep 17 00:00:00 2001 From: Beau Gunderson Date: Wed, 27 Mar 2024 14:25:55 -0600 Subject: [PATCH 12/13] move custom plugins for dev --- .../my_first_plugin/{my_first_plugin => commands}/__init__.py | 0 .../{my_first_plugin/commands => content}/__init__.py | 0 .../{my_first_plugin/content => effects}/__init__.py | 0 .../{my_first_plugin/effects => protocols}/__init__.py | 0 .../my_first_plugin/{my_first_plugin => }/protocols/protocol.py | 0 .../{my_first_plugin/protocols => views}/__init__.py | 0 .../views => my_second_plugin/commands}/__init__.py | 0 .../my_second_plugin/{my_second_plugin => content}/__init__.py | 0 .../{my_second_plugin/commands => effects}/__init__.py | 0 .../my_second_plugin/my_second_plugin/protocols/__init__.py | 0 .../my_second_plugin/my_second_plugin/views/__init__.py | 0 .../{my_second_plugin/content => protocols}/__init__.py | 0 .../my_second_plugin/{my_second_plugin => }/protocols/protocol.py | 0 .../{my_second_plugin/effects => views}/__init__.py | 0 14 files changed, 0 insertions(+), 0 deletions(-) rename custom-plugins/my_first_plugin/{my_first_plugin => commands}/__init__.py (100%) rename custom-plugins/my_first_plugin/{my_first_plugin/commands => content}/__init__.py (100%) rename custom-plugins/my_first_plugin/{my_first_plugin/content => effects}/__init__.py (100%) rename custom-plugins/my_first_plugin/{my_first_plugin/effects => protocols}/__init__.py (100%) rename custom-plugins/my_first_plugin/{my_first_plugin => }/protocols/protocol.py (100%) rename custom-plugins/my_first_plugin/{my_first_plugin/protocols => views}/__init__.py (100%) rename custom-plugins/{my_first_plugin/my_first_plugin/views => my_second_plugin/commands}/__init__.py (100%) rename custom-plugins/my_second_plugin/{my_second_plugin => content}/__init__.py (100%) rename custom-plugins/my_second_plugin/{my_second_plugin/commands => effects}/__init__.py (100%) delete mode 100644 custom-plugins/my_second_plugin/my_second_plugin/protocols/__init__.py delete mode 100644 custom-plugins/my_second_plugin/my_second_plugin/views/__init__.py rename custom-plugins/my_second_plugin/{my_second_plugin/content => protocols}/__init__.py (100%) rename custom-plugins/my_second_plugin/{my_second_plugin => }/protocols/protocol.py (100%) rename custom-plugins/my_second_plugin/{my_second_plugin/effects => views}/__init__.py (100%) diff --git a/custom-plugins/my_first_plugin/my_first_plugin/__init__.py b/custom-plugins/my_first_plugin/commands/__init__.py similarity index 100% rename from custom-plugins/my_first_plugin/my_first_plugin/__init__.py rename to custom-plugins/my_first_plugin/commands/__init__.py diff --git a/custom-plugins/my_first_plugin/my_first_plugin/commands/__init__.py b/custom-plugins/my_first_plugin/content/__init__.py similarity index 100% rename from custom-plugins/my_first_plugin/my_first_plugin/commands/__init__.py rename to custom-plugins/my_first_plugin/content/__init__.py diff --git a/custom-plugins/my_first_plugin/my_first_plugin/content/__init__.py b/custom-plugins/my_first_plugin/effects/__init__.py similarity index 100% rename from custom-plugins/my_first_plugin/my_first_plugin/content/__init__.py rename to custom-plugins/my_first_plugin/effects/__init__.py diff --git a/custom-plugins/my_first_plugin/my_first_plugin/effects/__init__.py b/custom-plugins/my_first_plugin/protocols/__init__.py similarity index 100% rename from custom-plugins/my_first_plugin/my_first_plugin/effects/__init__.py rename to custom-plugins/my_first_plugin/protocols/__init__.py diff --git a/custom-plugins/my_first_plugin/my_first_plugin/protocols/protocol.py b/custom-plugins/my_first_plugin/protocols/protocol.py similarity index 100% rename from custom-plugins/my_first_plugin/my_first_plugin/protocols/protocol.py rename to custom-plugins/my_first_plugin/protocols/protocol.py diff --git a/custom-plugins/my_first_plugin/my_first_plugin/protocols/__init__.py b/custom-plugins/my_first_plugin/views/__init__.py similarity index 100% rename from custom-plugins/my_first_plugin/my_first_plugin/protocols/__init__.py rename to custom-plugins/my_first_plugin/views/__init__.py diff --git a/custom-plugins/my_first_plugin/my_first_plugin/views/__init__.py b/custom-plugins/my_second_plugin/commands/__init__.py similarity index 100% rename from custom-plugins/my_first_plugin/my_first_plugin/views/__init__.py rename to custom-plugins/my_second_plugin/commands/__init__.py diff --git a/custom-plugins/my_second_plugin/my_second_plugin/__init__.py b/custom-plugins/my_second_plugin/content/__init__.py similarity index 100% rename from custom-plugins/my_second_plugin/my_second_plugin/__init__.py rename to custom-plugins/my_second_plugin/content/__init__.py diff --git a/custom-plugins/my_second_plugin/my_second_plugin/commands/__init__.py b/custom-plugins/my_second_plugin/effects/__init__.py similarity index 100% rename from custom-plugins/my_second_plugin/my_second_plugin/commands/__init__.py rename to custom-plugins/my_second_plugin/effects/__init__.py diff --git a/custom-plugins/my_second_plugin/my_second_plugin/protocols/__init__.py b/custom-plugins/my_second_plugin/my_second_plugin/protocols/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/custom-plugins/my_second_plugin/my_second_plugin/views/__init__.py b/custom-plugins/my_second_plugin/my_second_plugin/views/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/custom-plugins/my_second_plugin/my_second_plugin/content/__init__.py b/custom-plugins/my_second_plugin/protocols/__init__.py similarity index 100% rename from custom-plugins/my_second_plugin/my_second_plugin/content/__init__.py rename to custom-plugins/my_second_plugin/protocols/__init__.py diff --git a/custom-plugins/my_second_plugin/my_second_plugin/protocols/protocol.py b/custom-plugins/my_second_plugin/protocols/protocol.py similarity index 100% rename from custom-plugins/my_second_plugin/my_second_plugin/protocols/protocol.py rename to custom-plugins/my_second_plugin/protocols/protocol.py diff --git a/custom-plugins/my_second_plugin/my_second_plugin/effects/__init__.py b/custom-plugins/my_second_plugin/views/__init__.py similarity index 100% rename from custom-plugins/my_second_plugin/my_second_plugin/effects/__init__.py rename to custom-plugins/my_second_plugin/views/__init__.py From 5bec67b8fefa98ed3bd4252dcc05c36a9fe39d4e Mon Sep 17 00:00:00 2001 From: Beau Gunderson Date: Wed, 27 Mar 2024 18:08:07 -0600 Subject: [PATCH 13/13] load plugins from disk! --- .../my_first_plugin/CANVAS_MANIFEST.json | 2 +- .../my_second_plugin/CANVAS_MANIFEST.json | 2 +- plugin_runner/plugin_runner.py | 150 ++++++++++++------ 3 files changed, 107 insertions(+), 47 deletions(-) diff --git a/custom-plugins/my_first_plugin/CANVAS_MANIFEST.json b/custom-plugins/my_first_plugin/CANVAS_MANIFEST.json index eccc6c5b..3c9403ef 100644 --- a/custom-plugins/my_first_plugin/CANVAS_MANIFEST.json +++ b/custom-plugins/my_first_plugin/CANVAS_MANIFEST.json @@ -6,7 +6,7 @@ "components": { "protocols": [ { - "class": "my_first_plugin.protocols.path.to.ProtocolClass", + "class": "my_first_plugin.protocols.protocol:Protocol", "description": "A protocol that does xyz...", "data_access": { "event": "assess_condition_selected", diff --git a/custom-plugins/my_second_plugin/CANVAS_MANIFEST.json b/custom-plugins/my_second_plugin/CANVAS_MANIFEST.json index 4be1557f..69a063e7 100644 --- a/custom-plugins/my_second_plugin/CANVAS_MANIFEST.json +++ b/custom-plugins/my_second_plugin/CANVAS_MANIFEST.json @@ -6,7 +6,7 @@ "components": { "protocols": [ { - "class": "my_second_plugin.protocols.path.to.ProtocolClass", + "class": "my_second_plugin.protocols.protocol:Protocol", "description": "A protocol that does xyz...", "data_access": { "event": "assess_condition_selected", diff --git a/plugin_runner/plugin_runner.py b/plugin_runner/plugin_runner.py index c34d043c..5f2e8697 100644 --- a/plugin_runner/plugin_runner.py +++ b/plugin_runner/plugin_runner.py @@ -1,13 +1,14 @@ import asyncio -import json - import importlib +import json import logging +import os +import pathlib +import sys import grpc from generated.messages.effects_pb2 import Effect, EffectType -from generated.messages.events_pb2 import EventResponse, EventType from generated.messages.events_pb2 import Event, EventResponse, EventType from generated.messages.plugins_pb2 import ReloadPluginsRequest, ReloadPluginsResponse from generated.services.plugin_runner_pb2_grpc import ( @@ -15,38 +16,40 @@ add_PluginRunnerServicer_to_server, ) -# import my_first_plugin -# import my_second_plugin +ENV = os.getenv("ENV", "development") -LOADED_PLUGINS = { - "my_first_plugin": __import__("my_first_plugin.my_first_plugin.protocols.protocol"), - "my_second_plugin": __import__("my_second_plugin.my_second_plugin.protocols.protocol"), -} +IS_PRODUCTION = ENV == "production" -# TODO load and store plugins externally -# def get_loaded_plugins(): -# return { -# "my_first_plugin": my_first_plugin, -# "my_second_plugin": my_second_plugin -# } +MANIFEST_FILE_NAME = "CANVAS_MANIFEST.json" +# specify a local plugin directory for development +PLUGIN_DIRECTORY = "/plugin-runner/custom-plugins" if IS_PRODUCTION else "./custom-plugins" + +# when we import plugins we'll use the module name directly so we need to add the plugin +# directory to the path +sys.path.append(PLUGIN_DIRECTORY) + +# a global dictionary of loaded plugins +# TODO: create typings here for the subkeys +LOADED_PLUGINS = {} + +# a global dictionary of events to protocol class names +EVENT_PROTOCOL_MAP = {} -class PluginRunner(PluginRunnerServicer): - EVENT_PROTOCOL_MAP = {} +class PluginRunner(PluginRunnerServicer): def __init__(self) -> None: - # load_plugins() - self.refresh_event_type_map() super().__init__() async def HandleEvent(self, request: Event, context): event_name = EventType.Name(request.type) - relevant_plugins = self.EVENT_PROTOCOL_MAP.get(event_name, []) + relevant_plugins = EVENT_PROTOCOL_MAP.get(event_name, []) effect_list = [] + for plugin_name in relevant_plugins: - module = LOADED_PLUGINS.get(plugin_name) - protocol_class = getattr(module, plugin_name).protocols.protocol.Protocol + plugin = LOADED_PLUGINS[plugin_name] + protocol_class = plugin["class"] effects = protocol_class(request).compute() effect_list = [ @@ -56,44 +59,101 @@ async def HandleEvent(self, request: Event, context): ) for effect in effects ] + yield EventResponse(success=True, effects=effect_list) async def ReloadPlugins(self, request: ReloadPluginsRequest, context): try: - reload_plugins() + load_plugins() except ImportError: yield ReloadPluginsResponse(success=False) else: - self.refresh_event_type_map() yield ReloadPluginsResponse(success=True) - def refresh_event_type_map(self): - self.EVENT_PROTOCOL_MAP = {} - for name, module in LOADED_PLUGINS.items(): - protocol_class = None - try: - # protocol_file = importlib.import_module(f"{name}.{name}.protocols.protocol") - # breakpoint() - protocol_file = getattr(module, name) - protocol_class = protocol_file.protocols.protocol.Protocol - except ImportError: - continue - if protocol_class and hasattr(protocol_class, "RESPONDS_TO"): - self.EVENT_PROTOCOL_MAP[protocol_class.RESPONDS_TO] = [name] +def load_or_reload_plugin(path: pathlib.Path) -> None: + logging.info(f"Loading {path}") + + manifest_file = path / MANIFEST_FILE_NAME + manifest_json = manifest_file.read_text() + + # the name is the folder name underneath the plugins directory + name = path.stem + + try: + manifest_json = json.loads(manifest_json) + except Exception as e: + logging.warn(f'Unable to load plugin "{name}":', e) + return + + try: + protocols = manifest_json["components"]["protocols"] + except Exception as e: + logging.warn(f'Unable to load plugin "{name}":', e) + return + + for protocol in protocols: + protocol_module, protocol_class = protocol["class"].split(":") + name_and_class = f"{name}:{protocol_module}:{protocol_class}" + + if name_and_class in LOADED_PLUGINS: + logging.info(f"Reloading plugin: {name_and_class}") + + protocol_module = LOADED_PLUGINS[name_and_class]["module"] + + importlib.reload(protocol_module) + + LOADED_PLUGINS[name_and_class]["active"] = True + else: + logging.info(f"Reloading plugin: {name_and_class}") + + module = importlib.import_module(protocol_module) + + LOADED_PLUGINS[name_and_class] = { + "active": True, + "class": getattr(module, protocol_class), + "module": module, + "protocol": protocol, + } + + +def refresh_event_type_map(): + EVENT_PROTOCOL_MAP = {} + + for name, plugin in LOADED_PLUGINS.items(): + if hasattr(plugin["class"], "RESPONDS_TO"): + EVENT_PROTOCOL_MAP[plugin["class"].RESPONDS_TO] = [name] + + logging.info(EVENT_PROTOCOL_MAP) + + +def load_plugins(): + # first mark each plugin as inactive since we want to remove it from + # LOADED_PLUGINS if it no longer exists on disk + for plugin in LOADED_PLUGINS.values(): + plugin["active"] = False + + candidates = os.listdir(PLUGIN_DIRECTORY) + # convert to Paths + plugin_paths = [pathlib.Path(os.path.join(PLUGIN_DIRECTORY, name)) for name in candidates] -def reload_plugins(): - for name, module in LOADED_PLUGINS.items(): - logging.info(f"Reloading plugin: {name}") + # get all directories under the plugin directory + plugin_paths = [path for path in plugin_paths if path.is_dir()] - protocol_module = getattr(LOADED_PLUGINS[name], name).protocols.protocol + # filter to only the directories containing a manifest file + plugin_paths = [path for path in plugin_paths if (path / MANIFEST_FILE_NAME).exists()] - importlib.reload(protocol_module) + # load or reload each plugin + for plugin_path in plugin_paths: + load_or_reload_plugin(plugin_path) - protocol_class = getattr(LOADED_PLUGINS[name], name).protocols.protocol.Protocol + # if a plugin has been uninstalled/disabled remove it from LOADED_PLUGINS + for name, plugin in LOADED_PLUGINS.copy().items(): + if not plugin["active"]: + del LOADED_PLUGINS[name] - print(f"DEBUG: {name} {protocol_class.NARRATIVE_STRING}") + refresh_event_type_map() async def serve(): @@ -106,7 +166,7 @@ async def serve(): logging.info(f"Starting server, listening on port {port}") - reload_plugins() + load_plugins() await server.start() await server.wait_for_termination()