canvas 0.53.0__py3-none-any.whl → 0.53.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of canvas might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: canvas
3
- Version: 0.53.0
3
+ Version: 0.53.2
4
4
  Summary: SDK to customize event-driven actions in your Canvas instance
5
5
  Author-email: Canvas Team <engineering@canvasmedical.com>
6
6
  License-Expression: MIT
@@ -98,12 +98,12 @@ canvas_generated/messages/effects_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8gXHDCN
98
98
  canvas_generated/messages/events_pb2.py,sha256=T8aM_hyYLeWcrhtPO4bwQLQb31x36iNkZe9OJBAdcvs,55188
99
99
  canvas_generated/messages/events_pb2.pyi,sha256=h__s2YxcuA6YFooql3fl4VXAT_eT5Yoh028H_k5m59o,99437
100
100
  canvas_generated/messages/events_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8gXHDCNMhZD2VXqocC9l_gk,159
101
- canvas_generated/messages/plugins_pb2.py,sha256=j2yt9uoSlVMtWg-cAoOmxiT-XWbBrqqCBZXOxEz7z4g,1271
102
- canvas_generated/messages/plugins_pb2.pyi,sha256=G1seqytP8GlJJh-AL2CJ0VyUNlReEvK61C-Oh8QuGiE,501
101
+ canvas_generated/messages/plugins_pb2.py,sha256=5Hs1MSJVWSbkdID3Iir9Vvq3AH2Abt7MyLv0qzuA2MY,1989
102
+ canvas_generated/messages/plugins_pb2.pyi,sha256=0vHABVzAzqthoUrxFeGOZ4OmGQTIfD3mW4DZtjtq-YI,1277
103
103
  canvas_generated/messages/plugins_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8gXHDCNMhZD2VXqocC9l_gk,159
104
- canvas_generated/services/plugin_runner_pb2.py,sha256=RfAo_imYoSuoexq-1IHhMhXZgQpzq91pqugxsigL8NU,1557
104
+ canvas_generated/services/plugin_runner_pb2.py,sha256=yXJbhjxCJsbUhM3iKF_ACeNNBpD423jCE3v4Wr8Alig,1727
105
105
  canvas_generated/services/plugin_runner_pb2.pyi,sha256=1w-Pa4k7HtlmQAr7B6sgV64zdZplBKQKHN-S8bjwO3w,265
106
- canvas_generated/services/plugin_runner_pb2_grpc.py,sha256=EzJJVkP_AZ3dwBA7OxUito0NSalRmjjg8q9TZ_P18ww,4549
106
+ canvas_generated/services/plugin_runner_pb2_grpc.py,sha256=jBu1qk1Ud6SWKE1p-LSgrT0qknOW-B-ywes1UZuvOK0,8065
107
107
  canvas_sdk/__init__.py,sha256=3GEB9s-a9icf8c4JBEXbrsKYbagWNyn1TtJ50n1F3Z0,170
108
108
  canvas_sdk/base.py,sha256=vHSb9aKhvKFkQ0ROpIMqQWfPxQG1B7iBk2kGcFWoKEU,4125
109
109
  canvas_sdk/caching/__init__.py,sha256=YYXr5tEQlnwMgTXJbOG963tKSPiUOM4mUkX8wuiqp7U,17
@@ -137,7 +137,7 @@ canvas_sdk/commands/commands/medication_statement.py,sha256=1nH-N3sI1T_dLHXksGLH
137
137
  canvas_sdk/commands/commands/past_surgical_history.py,sha256=0mwybwfiAFE7elVXw1ichVitC3Jpl1JhPfNP89jhVhU,503
138
138
  canvas_sdk/commands/commands/perform.py,sha256=8o5q3snZ7TdkFFTsExZQWctXlpItSJfSihRQVZ3EEGo,409
139
139
  canvas_sdk/commands/commands/plan.py,sha256=2aJ9sUzP45sGw5ExHQToCkndidod5zwVx0a93LA1oRk,251
140
- canvas_sdk/commands/commands/prescribe.py,sha256=q4AHX4efshGcQmxIrEDOqcBNpkCuevtD6tKLInCgl28,8404
140
+ canvas_sdk/commands/commands/prescribe.py,sha256=nuCIDABvFmDUXsUUQl7TvLX-owvO7j5GBd6Gg75Rmr8,8556
141
141
  canvas_sdk/commands/commands/reason_for_visit.py,sha256=eEUnlmRa6yNjJ-_6oIHx18s_DKZxkgY0sv7CkyxIbVY,1986
142
142
  canvas_sdk/commands/commands/refer.py,sha256=uZFYxBajCiMSABEEztRM-jYxhOa1W2dw--fgi68mIOs,1620
143
143
  canvas_sdk/commands/commands/refill.py,sha256=4uTgwIryz_ab4pUGN_wgrEHlDGv__9ecOXOTuLJM6n8,1228
@@ -298,19 +298,19 @@ plugin_runner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
298
298
  plugin_runner/allowed-module-imports.json,sha256=j6-KDxn9oJLZXfLwXBSg35znDOUPHqluxu5CcOC1bOA,35350
299
299
  plugin_runner/authentication.py,sha256=UyyhXajokVFH866dpDhoTlXS9Cg7y0sQltn0_LcwXrY,1131
300
300
  plugin_runner/aws_headers.py,sha256=wZ8584E1fTW0CdGxOCnLSF8alH27z-URcUyoc6y6ohg,2782
301
- plugin_runner/exceptions.py,sha256=YnRZiQVzbU3HrVlmEXLje_np99009YnhTRVHHyBCtqc,433
301
+ plugin_runner/exceptions.py,sha256=ltqn56SMTg-T5miSh5hux4ojwx0hZGSWaB7BxyAmcAo,545
302
302
  plugin_runner/generate_allowed_imports.py,sha256=LQuVxL_j5n0Sj-KgR4Q8D9mj0xfuDqzO69kBfZUqwGE,2565
303
- plugin_runner/installation.py,sha256=LLjtnzPk-w4go3UbXnBItJTKz1ajR_5kGQbFXTaWTFU,7693
303
+ plugin_runner/installation.py,sha256=Cpt6FEhIEFColopp9xoCWGn0C3wrRF7BAMOK00W9ZSU,8489
304
304
  plugin_runner/load_all_plugins.py,sha256=4T2gW2YljhIx4xfwf1c0F_8oIbE1ubsLj0ShkHRtlVY,5847
305
- plugin_runner/plugin_runner.py,sha256=PqtvyUHOSvIHRW97zX_NKEjszJ1GVmETXxkZhzkZoe0,21975
305
+ plugin_runner/plugin_runner.py,sha256=ldijT97b5zsnMvr5XxUZ_1wMhVSZ7epvE1ueCrUG5wc,25165
306
306
  plugin_runner/sandbox.py,sha256=3xPlDuCHuJkb-K41gV8uABOn-Hla54qkq9iSafP2LVI,30210
307
307
  protobufs/canvas_generated/messages/effects.proto,sha256=zTCelFeZ2ajsQPadiWFPOfqqpEB2ekSiLrdqmSd6tbY,9316
308
308
  protobufs/canvas_generated/messages/events.proto,sha256=XBMsexTQb_4ZnvRF_u4ghEfKpuHN7eU6CJJmHGM-ThU,51507
309
- protobufs/canvas_generated/messages/plugins.proto,sha256=oNainUPWFYQjgCX7bJEPI9_VnHC5VZduzOqgR4Q7dNM,109
310
- protobufs/canvas_generated/services/plugin_runner.proto,sha256=doadBKn5k4xAtOgR-q_pEvW4yzxpUaHNOowMG6CL5GY,304
309
+ protobufs/canvas_generated/messages/plugins.proto,sha256=xJyEeTwM6wWja3vGECLsIzfabwf1VahDgacoPL3TuwI,323
310
+ protobufs/canvas_generated/services/plugin_runner.proto,sha256=PZ0Ts11b9tdA5Gkg2M05JVEKAm0R4LFEwrGRS-TQ16E,466
311
311
  pubsub/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
312
312
  pubsub/pubsub.py,sha256=PHIvJ5SD3M-jQSYeGGSj1FuG6CvP6BQffAoGax9Uudk,1423
313
- canvas-0.53.0.dist-info/METADATA,sha256=abVbeams2cH3qPdbzfknUGPGciSjSF6hcOi-26yxHU4,4645
314
- canvas-0.53.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
315
- canvas-0.53.0.dist-info/entry_points.txt,sha256=0Vs_9GmTVUNniH6eDBlRPgofmADMV4BES6Ao26M4AbM,47
316
- canvas-0.53.0.dist-info/RECORD,,
313
+ canvas-0.53.2.dist-info/METADATA,sha256=i5-M9AqUD6vN0_AkeoM-J0bgr3veI232-eW-T4_mxvo,4645
314
+ canvas-0.53.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
315
+ canvas-0.53.2.dist-info/entry_points.txt,sha256=0Vs_9GmTVUNniH6eDBlRPgofmADMV4BES6Ao26M4AbM,47
316
+ canvas-0.53.2.dist-info/RECORD,,
@@ -14,7 +14,7 @@ _sym_db = _symbol_database.Default()
14
14
 
15
15
 
16
16
 
17
- DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\'canvas_generated/messages/plugins.proto\"\x16\n\x14ReloadPluginsRequest\"(\n\x15ReloadPluginsResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x62\x06proto3')
17
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\'canvas_generated/messages/plugins.proto\"\x16\n\x14ReloadPluginsRequest\"(\n\x15ReloadPluginsResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"%\n\x13ReloadPluginRequest\x12\x0e\n\x06plugin\x18\x01 \x01(\t\"\'\n\x14ReloadPluginResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"%\n\x13UnloadPluginRequest\x12\x0e\n\x06plugin\x18\x01 \x01(\t\"\'\n\x14UnloadPluginResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x62\x06proto3')
18
18
 
19
19
  _globals = globals()
20
20
  _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -25,4 +25,12 @@ if _descriptor._USE_C_DESCRIPTORS == False:
25
25
  _globals['_RELOADPLUGINSREQUEST']._serialized_end=65
26
26
  _globals['_RELOADPLUGINSRESPONSE']._serialized_start=67
27
27
  _globals['_RELOADPLUGINSRESPONSE']._serialized_end=107
28
+ _globals['_RELOADPLUGINREQUEST']._serialized_start=109
29
+ _globals['_RELOADPLUGINREQUEST']._serialized_end=146
30
+ _globals['_RELOADPLUGINRESPONSE']._serialized_start=148
31
+ _globals['_RELOADPLUGINRESPONSE']._serialized_end=187
32
+ _globals['_UNLOADPLUGINREQUEST']._serialized_start=189
33
+ _globals['_UNLOADPLUGINREQUEST']._serialized_end=226
34
+ _globals['_UNLOADPLUGINRESPONSE']._serialized_start=228
35
+ _globals['_UNLOADPLUGINRESPONSE']._serialized_end=267
28
36
  # @@protoc_insertion_point(module_scope)
@@ -13,3 +13,27 @@ class ReloadPluginsResponse(_message.Message):
13
13
  SUCCESS_FIELD_NUMBER: _ClassVar[int]
14
14
  success: bool
15
15
  def __init__(self, success: bool = ...) -> None: ...
16
+
17
+ class ReloadPluginRequest(_message.Message):
18
+ __slots__ = ("plugin",)
19
+ PLUGIN_FIELD_NUMBER: _ClassVar[int]
20
+ plugin: str
21
+ def __init__(self, plugin: _Optional[str] = ...) -> None: ...
22
+
23
+ class ReloadPluginResponse(_message.Message):
24
+ __slots__ = ("success",)
25
+ SUCCESS_FIELD_NUMBER: _ClassVar[int]
26
+ success: bool
27
+ def __init__(self, success: bool = ...) -> None: ...
28
+
29
+ class UnloadPluginRequest(_message.Message):
30
+ __slots__ = ("plugin",)
31
+ PLUGIN_FIELD_NUMBER: _ClassVar[int]
32
+ plugin: str
33
+ def __init__(self, plugin: _Optional[str] = ...) -> None: ...
34
+
35
+ class UnloadPluginResponse(_message.Message):
36
+ __slots__ = ("success",)
37
+ SUCCESS_FIELD_NUMBER: _ClassVar[int]
38
+ success: bool
39
+ def __init__(self, success: bool = ...) -> None: ...
@@ -16,7 +16,7 @@ from canvas_generated.messages import events_pb2 as canvas__generated_dot_messag
16
16
  from canvas_generated.messages import plugins_pb2 as canvas__generated_dot_messages_dot_plugins__pb2
17
17
 
18
18
 
19
- DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n-canvas_generated/services/plugin_runner.proto\x12\x06\x63\x61nvas\x1a&canvas_generated/messages/events.proto\x1a\'canvas_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')
19
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n-canvas_generated/services/plugin_runner.proto\x12\x06\x63\x61nvas\x1a&canvas_generated/messages/events.proto\x1a\'canvas_generated/messages/plugins.proto2\x85\x02\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\x12=\n\x0cReloadPlugin\x12\x14.ReloadPluginRequest\x1a\x15.ReloadPluginResponse0\x01\x12=\n\x0cUnloadPlugin\x12\x14.UnloadPluginRequest\x1a\x15.UnloadPluginResponse0\x01\x62\x06proto3')
20
20
 
21
21
  _globals = globals()
22
22
  _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -24,5 +24,5 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'canvas_generated.services.p
24
24
  if _descriptor._USE_C_DESCRIPTORS == False:
25
25
  DESCRIPTOR._options = None
26
26
  _globals['_PLUGINRUNNER']._serialized_start=139
27
- _globals['_PLUGINRUNNER']._serialized_end=274
27
+ _globals['_PLUGINRUNNER']._serialized_end=400
28
28
  # @@protoc_insertion_point(module_scope)
@@ -25,6 +25,16 @@ class PluginRunnerStub(object):
25
25
  request_serializer=canvas__generated_dot_messages_dot_plugins__pb2.ReloadPluginsRequest.SerializeToString,
26
26
  response_deserializer=canvas__generated_dot_messages_dot_plugins__pb2.ReloadPluginsResponse.FromString,
27
27
  )
28
+ self.ReloadPlugin = channel.unary_stream(
29
+ '/canvas.PluginRunner/ReloadPlugin',
30
+ request_serializer=canvas__generated_dot_messages_dot_plugins__pb2.ReloadPluginRequest.SerializeToString,
31
+ response_deserializer=canvas__generated_dot_messages_dot_plugins__pb2.ReloadPluginResponse.FromString,
32
+ )
33
+ self.UnloadPlugin = channel.unary_stream(
34
+ '/canvas.PluginRunner/UnloadPlugin',
35
+ request_serializer=canvas__generated_dot_messages_dot_plugins__pb2.UnloadPluginRequest.SerializeToString,
36
+ response_deserializer=canvas__generated_dot_messages_dot_plugins__pb2.UnloadPluginResponse.FromString,
37
+ )
28
38
 
29
39
 
30
40
  class PluginRunnerServicer(object):
@@ -42,6 +52,18 @@ class PluginRunnerServicer(object):
42
52
  context.set_details('Method not implemented!')
43
53
  raise NotImplementedError('Method not implemented!')
44
54
 
55
+ def ReloadPlugin(self, request, context):
56
+ """Missing associated documentation comment in .proto file."""
57
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
58
+ context.set_details('Method not implemented!')
59
+ raise NotImplementedError('Method not implemented!')
60
+
61
+ def UnloadPlugin(self, request, context):
62
+ """Missing associated documentation comment in .proto file."""
63
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
64
+ context.set_details('Method not implemented!')
65
+ raise NotImplementedError('Method not implemented!')
66
+
45
67
 
46
68
  def add_PluginRunnerServicer_to_server(servicer, server):
47
69
  rpc_method_handlers = {
@@ -55,6 +77,16 @@ def add_PluginRunnerServicer_to_server(servicer, server):
55
77
  request_deserializer=canvas__generated_dot_messages_dot_plugins__pb2.ReloadPluginsRequest.FromString,
56
78
  response_serializer=canvas__generated_dot_messages_dot_plugins__pb2.ReloadPluginsResponse.SerializeToString,
57
79
  ),
80
+ 'ReloadPlugin': grpc.unary_stream_rpc_method_handler(
81
+ servicer.ReloadPlugin,
82
+ request_deserializer=canvas__generated_dot_messages_dot_plugins__pb2.ReloadPluginRequest.FromString,
83
+ response_serializer=canvas__generated_dot_messages_dot_plugins__pb2.ReloadPluginResponse.SerializeToString,
84
+ ),
85
+ 'UnloadPlugin': grpc.unary_stream_rpc_method_handler(
86
+ servicer.UnloadPlugin,
87
+ request_deserializer=canvas__generated_dot_messages_dot_plugins__pb2.UnloadPluginRequest.FromString,
88
+ response_serializer=canvas__generated_dot_messages_dot_plugins__pb2.UnloadPluginResponse.SerializeToString,
89
+ ),
58
90
  }
59
91
  generic_handler = grpc.method_handlers_generic_handler(
60
92
  'canvas.PluginRunner', rpc_method_handlers)
@@ -98,3 +130,37 @@ class PluginRunner(object):
98
130
  canvas__generated_dot_messages_dot_plugins__pb2.ReloadPluginsResponse.FromString,
99
131
  options, channel_credentials,
100
132
  insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
133
+
134
+ @staticmethod
135
+ def ReloadPlugin(request,
136
+ target,
137
+ options=(),
138
+ channel_credentials=None,
139
+ call_credentials=None,
140
+ insecure=False,
141
+ compression=None,
142
+ wait_for_ready=None,
143
+ timeout=None,
144
+ metadata=None):
145
+ return grpc.experimental.unary_stream(request, target, '/canvas.PluginRunner/ReloadPlugin',
146
+ canvas__generated_dot_messages_dot_plugins__pb2.ReloadPluginRequest.SerializeToString,
147
+ canvas__generated_dot_messages_dot_plugins__pb2.ReloadPluginResponse.FromString,
148
+ options, channel_credentials,
149
+ insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
150
+
151
+ @staticmethod
152
+ def UnloadPlugin(request,
153
+ target,
154
+ options=(),
155
+ channel_credentials=None,
156
+ call_credentials=None,
157
+ insecure=False,
158
+ compression=None,
159
+ wait_for_ready=None,
160
+ timeout=None,
161
+ metadata=None):
162
+ return grpc.experimental.unary_stream(request, target, '/canvas.PluginRunner/UnloadPlugin',
163
+ canvas__generated_dot_messages_dot_plugins__pb2.UnloadPluginRequest.SerializeToString,
164
+ canvas__generated_dot_messages_dot_plugins__pb2.UnloadPluginResponse.FromString,
165
+ options, channel_credentials,
166
+ insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@@ -170,6 +170,9 @@ class PrescribeCommand(_SendableCommandMixin, _BaseCommand):
170
170
  if isinstance(compound_data, CompoundMedicationData):
171
171
  values["compound_medication_values"] = compound_data.to_dict()
172
172
 
173
+ if values.get("fdb_code") is not None and values.get("compound_medication_values") == {}:
174
+ del values["compound_medication_values"]
175
+
173
176
  return values
174
177
 
175
178
  def originate(self, line_number: int = -1) -> Effect:
@@ -12,3 +12,7 @@ class InvalidPluginFormat(PluginValidationError):
12
12
 
13
13
  class PluginInstallationError(PluginError):
14
14
  """An exception raised when a plugin fails to install."""
15
+
16
+
17
+ class PluginUninstallationError(PluginError):
18
+ """An exception raised when a plugin fails to uninstall."""
@@ -17,7 +17,11 @@ from psycopg.rows import dict_row
17
17
 
18
18
  from logger import log
19
19
  from plugin_runner.aws_headers import aws_sig_v4_headers
20
- from plugin_runner.exceptions import InvalidPluginFormat, PluginInstallationError
20
+ from plugin_runner.exceptions import (
21
+ InvalidPluginFormat,
22
+ PluginInstallationError,
23
+ PluginUninstallationError,
24
+ )
21
25
  from settings import (
22
26
  AWS_ACCESS_KEY_ID,
23
27
  AWS_REGION,
@@ -68,15 +72,28 @@ class PluginAttributes(TypedDict):
68
72
  secrets: dict[str, str]
69
73
 
70
74
 
71
- def enabled_plugins() -> dict[str, PluginAttributes]:
72
- """Returns a dictionary of enabled plugins and their attributes."""
75
+ def enabled_plugins(plugin_names: list[str] | None = None) -> dict[str, PluginAttributes]:
76
+ """Returns a dictionary of enabled plugins and their attributes.
77
+
78
+ If `plugin_names` is provided, only returns those plugins (if enabled).
79
+ """
73
80
  conn = open_database_connection()
74
81
 
75
82
  with conn.cursor(row_factory=dict_row) as cursor:
76
- cursor.execute(
77
- "SELECT name, package, version, key, value FROM plugin_io_plugin p "
78
- "LEFT JOIN plugin_io_pluginsecret s ON p.id = s.plugin_id WHERE is_enabled"
83
+ base_query = (
84
+ "SELECT name, package, version, key, value "
85
+ "FROM plugin_io_plugin p "
86
+ "LEFT JOIN plugin_io_pluginsecret s ON p.id = s.plugin_id "
87
+ "WHERE is_enabled"
79
88
  )
89
+
90
+ params = []
91
+ if plugin_names:
92
+ placeholders = ",".join(["%s"] * len(plugin_names))
93
+ base_query += f" AND name IN ({placeholders})"
94
+ params.extend(plugin_names)
95
+
96
+ cursor.execute(base_query, params)
80
97
  rows = cursor.fetchall()
81
98
  plugins = _extract_rows_to_dict(rows)
82
99
 
@@ -210,22 +227,31 @@ def disable_plugin(plugin_name: str) -> None:
210
227
 
211
228
  def uninstall_plugin(plugin_name: str) -> None:
212
229
  """Remove the plugin from the filesystem."""
213
- log.info(f'Uninstalling plugin "{plugin_name}"')
230
+ try:
231
+ log.info(f'Uninstalling plugin "{plugin_name}"')
214
232
 
215
- plugin_path = Path(PLUGIN_DIRECTORY) / plugin_name
233
+ plugin_path = Path(PLUGIN_DIRECTORY) / plugin_name
216
234
 
217
- if plugin_path.exists():
218
- shutil.rmtree(plugin_path)
235
+ if plugin_path.exists():
236
+ shutil.rmtree(plugin_path)
237
+ except Exception as e:
238
+ raise PluginUninstallationError() from e
219
239
 
220
240
 
221
241
  def install_plugins() -> None:
222
242
  """Install all enabled plugins."""
223
243
  log.info("Installing plugins")
244
+ try:
245
+ plugins_dir = Path(PLUGIN_DIRECTORY).resolve()
224
246
 
225
- if Path(PLUGIN_DIRECTORY).exists():
226
- shutil.rmtree(PLUGIN_DIRECTORY)
247
+ if plugins_dir.exists():
248
+ shutil.rmtree(plugins_dir.as_posix())
227
249
 
228
- os.mkdir(PLUGIN_DIRECTORY)
250
+ plugins_dir.mkdir(parents=False, exist_ok=True)
251
+ except Exception as e:
252
+ raise PluginInstallationError(
253
+ f'Failed to reset plugin directory "{PLUGIN_DIRECTORY}": {e}"'
254
+ ) from e
229
255
 
230
256
  for plugin_name, attributes in enabled_plugins().items():
231
257
  try:
@@ -24,7 +24,14 @@ from sentry_sdk.integrations.logging import ignore_logger
24
24
 
25
25
  import settings
26
26
  from canvas_generated.messages.effects_pb2 import EffectType
27
- from canvas_generated.messages.plugins_pb2 import ReloadPluginsRequest, ReloadPluginsResponse
27
+ from canvas_generated.messages.plugins_pb2 import (
28
+ ReloadPluginRequest,
29
+ ReloadPluginResponse,
30
+ ReloadPluginsRequest,
31
+ ReloadPluginsResponse,
32
+ UnloadPluginRequest,
33
+ UnloadPluginResponse,
34
+ )
28
35
  from canvas_generated.services.plugin_runner_pb2_grpc import (
29
36
  PluginRunnerServicer,
30
37
  add_PluginRunnerServicer_to_server,
@@ -38,7 +45,13 @@ from canvas_sdk.utils import metrics
38
45
  from canvas_sdk.utils.metrics import measured
39
46
  from logger import log
40
47
  from plugin_runner.authentication import token_for_plugin
41
- from plugin_runner.installation import install_plugins
48
+ from plugin_runner.exceptions import PluginInstallationError, PluginUninstallationError
49
+ from plugin_runner.installation import (
50
+ enabled_plugins,
51
+ install_plugin,
52
+ install_plugins,
53
+ uninstall_plugin,
54
+ )
42
55
  from plugin_runner.sandbox import Sandbox, sandbox_from_module
43
56
  from settings import (
44
57
  CHANNEL_NAME,
@@ -305,14 +318,51 @@ class PluginRunner(PluginRunnerServicer):
305
318
  self, request: ReloadPluginsRequest, context: Any
306
319
  ) -> Iterable[ReloadPluginsResponse]:
307
320
  """This is invoked when we need to reload plugins."""
308
- log.info("Reloading plugins...")
321
+ log.info("Reloading all plugins...")
322
+
323
+ message = {"action": "reload"}
324
+
309
325
  try:
310
- publish_message(message={"action": "reload"})
326
+ publish_message(message=message)
311
327
  except ImportError:
312
328
  yield ReloadPluginsResponse(success=False)
313
329
  else:
314
330
  yield ReloadPluginsResponse(success=True)
315
331
 
332
+ def ReloadPlugin(
333
+ self, request: ReloadPluginRequest, context: Any
334
+ ) -> Iterable[ReloadPluginResponse]:
335
+ """This is invoked when we need to reload a specific plugin."""
336
+ log.info(f'Reloading plugin "{request.plugin}"...')
337
+
338
+ message = {
339
+ "action": "reload",
340
+ "plugin": request.plugin,
341
+ }
342
+ try:
343
+ publish_message(message=message)
344
+ except ImportError:
345
+ yield ReloadPluginResponse(success=False)
346
+ else:
347
+ yield ReloadPluginResponse(success=True)
348
+
349
+ def UnloadPlugin(
350
+ self, request: UnloadPluginRequest, context: Any
351
+ ) -> Iterable[UnloadPluginResponse]:
352
+ """This is invoked when we need to reload a specific plugin."""
353
+ log.info(f'Unloading plugin "{request.plugin}"...')
354
+
355
+ message = {
356
+ "action": "unload",
357
+ "plugin": request.plugin,
358
+ }
359
+ try:
360
+ publish_message(message=message)
361
+ except ImportError:
362
+ yield UnloadPluginResponse(success=False)
363
+ else:
364
+ yield UnloadPluginResponse(success=True)
365
+
316
366
 
317
367
  STOP_SYNCHRONIZER = threading.Event()
318
368
 
@@ -346,20 +396,40 @@ def synchronize_plugins(run_once: bool = False) -> None:
346
396
  if "action" not in data:
347
397
  continue
348
398
 
349
- if data["action"] == "reload":
350
- log.info("synchronize_plugins: installing/reloading plugins for action=reload")
399
+ plugin_name = data.get("plugin", None)
400
+ try:
401
+ if data["action"] == "reload":
402
+ if plugin_name:
403
+ plugin = enabled_plugins([plugin_name]).get(plugin_name, None)
404
+
405
+ if plugin:
406
+ log.info(
407
+ f'synchronize_plugins: installing/reloading plugin "{plugin_name}" for action=reload'
408
+ )
409
+ install_plugin(plugin_name, attributes=plugin)
410
+ plugin_dir = pathlib.Path(PLUGIN_DIRECTORY) / plugin_name
411
+ load_plugin(plugin_dir.resolve())
412
+ else:
413
+ log.info("synchronize_plugins: installing/reloading plugins for action=reload")
414
+ install_plugins()
415
+ load_plugins()
416
+ elif data["action"] == "unload" and plugin_name:
417
+ log.info(f'synchronize_plugins: uninstalling plugin "{plugin_name}"')
418
+ unload_plugin(plugin_name)
419
+ uninstall_plugin(plugin_name)
420
+ except Exception as e:
421
+ if isinstance(e, PluginInstallationError):
422
+ message = "install_plugins failed"
423
+ elif isinstance(e, PluginUninstallationError):
424
+ message = "uninstall_plugin failed"
425
+ else:
426
+ message = "load_plugins failed"
351
427
 
352
- try:
353
- install_plugins()
354
- except Exception as e:
355
- log.error(f"synchronize_plugins: install_plugins failed: {e}")
356
- sentry_sdk.capture_exception(e)
428
+ if plugin_name:
429
+ message += f' for plugin "{plugin_name}"'
357
430
 
358
- try:
359
- load_plugins()
360
- except Exception as e:
361
- log.error(f"synchronize_plugins: load_plugins failed: {e}")
362
- sentry_sdk.capture_exception(e)
431
+ log.error(f"synchronize_plugins: {message}: {e}")
432
+ sentry_sdk.capture_exception(e)
363
433
 
364
434
  if run_once:
365
435
  break
@@ -523,7 +593,7 @@ def load_or_reload_plugin(path: pathlib.Path) -> bool:
523
593
  result = sandbox.execute()
524
594
 
525
595
  if name_and_class in LOADED_PLUGINS:
526
- log.info(f"Reloading plugin '{name_and_class}'")
596
+ log.info(f"Reloading handler '{name_and_class}'")
527
597
 
528
598
  LOADED_PLUGINS[name_and_class]["active"] = True
529
599
 
@@ -531,7 +601,7 @@ def load_or_reload_plugin(path: pathlib.Path) -> bool:
531
601
  LOADED_PLUGINS[name_and_class]["sandbox"] = result
532
602
  LOADED_PLUGINS[name_and_class]["secrets"] = secrets_json
533
603
  else:
534
- log.info(f"Loading plugin '{name_and_class}'")
604
+ log.info(f'Loading handler "{name_and_class}"')
535
605
 
536
606
  LOADED_PLUGINS[name_and_class] = {
537
607
  "active": True,
@@ -553,6 +623,23 @@ def load_or_reload_plugin(path: pathlib.Path) -> bool:
553
623
  return not any_failed
554
624
 
555
625
 
626
+ def unload_plugin(name: str) -> None:
627
+ """Unload a plugin by its name."""
628
+ handlers_removed = False
629
+
630
+ for handler_name in LOADED_PLUGINS.copy():
631
+ if handler_name.startswith(f"{name}:"):
632
+ log.info(f'Unloading handler "{handler_name}"')
633
+ del LOADED_PLUGINS[handler_name]
634
+ handlers_removed = True
635
+
636
+ if handlers_removed:
637
+ # Refresh the event type map to remove any handlers for the unloaded plugin
638
+ refresh_event_type_map()
639
+ else:
640
+ log.warning(f"No handlers found for plugin '{name}' to unload.")
641
+
642
+
556
643
  def refresh_event_type_map() -> None:
557
644
  """Ensure the event subscriptions are up to date."""
558
645
  EVENT_HANDLER_MAP.clear()
@@ -608,6 +695,13 @@ def load_plugins(specified_plugin_paths: list[str] | None = None) -> None:
608
695
  refresh_event_type_map()
609
696
 
610
697
 
698
+ @measured
699
+ def load_plugin(path: pathlib.Path) -> None:
700
+ """Load a plugin from the specified path."""
701
+ load_or_reload_plugin(path)
702
+ refresh_event_type_map()
703
+
704
+
611
705
  # NOTE: specified_plugin_paths powers the `canvas run-plugins` command
612
706
  def main(specified_plugin_paths: list[str] | None = None) -> None:
613
707
  """Run the server and the synchronize_plugins loop."""
@@ -1,9 +1,24 @@
1
1
  syntax = "proto3";
2
2
 
3
3
  message ReloadPluginsRequest {
4
-
5
4
  }
6
5
 
7
6
  message ReloadPluginsResponse {
8
7
  bool success = 1;
9
8
  }
9
+
10
+ message ReloadPluginRequest {
11
+ string plugin = 1;
12
+ }
13
+
14
+ message ReloadPluginResponse {
15
+ bool success = 1;
16
+ }
17
+
18
+ message UnloadPluginRequest {
19
+ string plugin = 1;
20
+ }
21
+
22
+ message UnloadPluginResponse {
23
+ bool success = 1;
24
+ }
@@ -9,4 +9,8 @@ service PluginRunner {
9
9
  rpc HandleEvent (Event) returns (stream EventResponse);
10
10
 
11
11
  rpc ReloadPlugins (ReloadPluginsRequest) returns (stream ReloadPluginsResponse);
12
+
13
+ rpc ReloadPlugin (ReloadPluginRequest) returns (stream ReloadPluginResponse);
14
+
15
+ rpc UnloadPlugin (UnloadPluginRequest) returns (stream UnloadPluginResponse);
12
16
  }