canvas 0.53.1__py3-none-any.whl → 0.54.0__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.
- {canvas-0.53.1.dist-info → canvas-0.54.0.dist-info}/METADATA +1 -1
- {canvas-0.53.1.dist-info → canvas-0.54.0.dist-info}/RECORD +32 -28
- canvas_cli/apps/plugin/plugin.py +40 -8
- canvas_generated/messages/effects_pb2.py +2 -2
- canvas_generated/messages/effects_pb2.pyi +12 -0
- canvas_generated/messages/events_pb2.py +2 -2
- canvas_generated/messages/events_pb2.pyi +12 -0
- canvas_generated/messages/plugins_pb2.py +9 -1
- canvas_generated/messages/plugins_pb2.pyi +24 -0
- canvas_generated/services/plugin_runner_pb2.py +2 -2
- canvas_generated/services/plugin_runner_pb2_grpc.py +66 -0
- canvas_sdk/caching/client.py +3 -1
- canvas_sdk/caching/plugins.py +7 -4
- canvas_sdk/effects/payment_processor.py +137 -0
- canvas_sdk/handlers/payment_processors/__init__.py +1 -0
- canvas_sdk/handlers/payment_processors/base.py +93 -0
- canvas_sdk/handlers/payment_processors/card.py +200 -0
- canvas_sdk/questionnaires/utils.py +2 -2
- canvas_sdk/templates/utils.py +5 -3
- canvas_sdk/utils/plugins.py +33 -20
- canvas_sdk/v1/data/patient.py +7 -0
- plugin_runner/allowed-module-imports.json +14 -0
- plugin_runner/exceptions.py +4 -0
- plugin_runner/installation.py +43 -14
- plugin_runner/plugin_runner.py +135 -18
- plugin_runner/sandbox.py +1 -0
- protobufs/canvas_generated/messages/effects.proto +8 -0
- protobufs/canvas_generated/messages/events.proto +8 -0
- protobufs/canvas_generated/messages/plugins.proto +16 -1
- protobufs/canvas_generated/services/plugin_runner.proto +4 -0
- {canvas-0.53.1.dist-info → canvas-0.54.0.dist-info}/WHEEL +0 -0
- {canvas-0.53.1.dist-info → canvas-0.54.0.dist-info}/entry_points.txt +0 -0
canvas_sdk/utils/plugins.py
CHANGED
|
@@ -8,19 +8,39 @@ from canvas_sdk.utils.metrics import measured
|
|
|
8
8
|
from settings import PLUGIN_DIRECTORY
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
def find_plugin_ancestor(frame: FrameType | None, max_depth: int = 10) -> FrameType | None:
|
|
12
|
+
"""
|
|
13
|
+
Recurse backwards to find any plugin ancestor of this frame.
|
|
14
|
+
"""
|
|
15
|
+
parent_frame = frame.f_back if frame else None
|
|
16
|
+
|
|
17
|
+
if not parent_frame:
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
if max_depth == 0:
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
if "__is_plugin__" in parent_frame.f_globals:
|
|
24
|
+
return parent_frame
|
|
25
|
+
|
|
26
|
+
return find_plugin_ancestor(frame=parent_frame, max_depth=max_depth - 1)
|
|
27
|
+
|
|
28
|
+
|
|
11
29
|
@measured
|
|
12
|
-
def
|
|
30
|
+
def plugin_context(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
13
31
|
"""Decorator to restrict a function's execution to plugins only."""
|
|
14
32
|
|
|
15
33
|
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
16
|
-
|
|
17
|
-
caller = current_frame.f_back if current_frame else None
|
|
34
|
+
plugin_frame = find_plugin_ancestor(inspect.currentframe())
|
|
18
35
|
|
|
19
|
-
if not
|
|
20
|
-
|
|
36
|
+
if not plugin_frame:
|
|
37
|
+
raise RuntimeError(
|
|
38
|
+
"Method that expected plugin context was called from outside a plugin."
|
|
39
|
+
)
|
|
21
40
|
|
|
22
|
-
plugin_name =
|
|
41
|
+
plugin_name = plugin_frame.f_globals["__name__"].split(".")[0]
|
|
23
42
|
plugin_dir = Path(PLUGIN_DIRECTORY) / plugin_name
|
|
43
|
+
|
|
24
44
|
kwargs["plugin_name"] = plugin_name
|
|
25
45
|
kwargs["plugin_dir"] = plugin_dir.resolve()
|
|
26
46
|
|
|
@@ -30,24 +50,17 @@ def plugin_only(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
|
30
50
|
|
|
31
51
|
|
|
32
52
|
@measured
|
|
33
|
-
def is_plugin_caller(
|
|
53
|
+
def is_plugin_caller() -> tuple[bool, str | None]:
|
|
34
54
|
"""Check if a function is called from a plugin."""
|
|
35
|
-
|
|
36
|
-
caller = current_frame.f_back if current_frame else None
|
|
37
|
-
|
|
38
|
-
if not caller:
|
|
39
|
-
return False, None
|
|
55
|
+
plugin_frame = find_plugin_ancestor(inspect.currentframe())
|
|
40
56
|
|
|
41
|
-
if
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
else:
|
|
45
|
-
return False, None
|
|
57
|
+
if plugin_frame:
|
|
58
|
+
module = plugin_frame.f_globals.get("__name__")
|
|
59
|
+
qualname = plugin_frame.f_code.co_qualname
|
|
46
60
|
|
|
47
|
-
|
|
48
|
-
qualname = caller.f_code.co_qualname
|
|
61
|
+
return True, f"{module}.{qualname}"
|
|
49
62
|
|
|
50
|
-
return
|
|
63
|
+
return False, None
|
|
51
64
|
|
|
52
65
|
|
|
53
66
|
__exports__ = ()
|
canvas_sdk/v1/data/patient.py
CHANGED
|
@@ -99,6 +99,13 @@ class Patient(Model):
|
|
|
99
99
|
def __str__(self) -> str:
|
|
100
100
|
return f"{self.first_name} {self.last_name}"
|
|
101
101
|
|
|
102
|
+
@property
|
|
103
|
+
def full_name(self) -> str:
|
|
104
|
+
"""Returns the patient's full name."""
|
|
105
|
+
return " ".join(
|
|
106
|
+
n for n in (self.first_name, self.middle_name, self.last_name, self.suffix) if n
|
|
107
|
+
)
|
|
108
|
+
|
|
102
109
|
def age_at(self, time: arrow.Arrow) -> float:
|
|
103
110
|
"""Given a datetime, returns what the patient's age would be at that datetime."""
|
|
104
111
|
age = float(0)
|
|
@@ -279,6 +279,14 @@
|
|
|
279
279
|
"canvas_sdk.effects.patient_profile_configuration": [
|
|
280
280
|
"PatientProfileConfiguration"
|
|
281
281
|
],
|
|
282
|
+
"canvas_sdk.effects.payment_processor": [
|
|
283
|
+
"AddPaymentMethodResponse",
|
|
284
|
+
"CardTransaction",
|
|
285
|
+
"PaymentMethod",
|
|
286
|
+
"PaymentProcessorForm",
|
|
287
|
+
"PaymentProcessorMetadata",
|
|
288
|
+
"RemovePaymentMethodResponse"
|
|
289
|
+
],
|
|
282
290
|
"canvas_sdk.effects.protocol_card": [
|
|
283
291
|
"ProtocolCard",
|
|
284
292
|
"Recommendation"
|
|
@@ -364,6 +372,12 @@
|
|
|
364
372
|
"canvas_sdk.handlers.cron_task": [
|
|
365
373
|
"CronTask"
|
|
366
374
|
],
|
|
375
|
+
"canvas_sdk.handlers.payment_processors.base": [
|
|
376
|
+
"PaymentProcessor"
|
|
377
|
+
],
|
|
378
|
+
"canvas_sdk.handlers.payment_processors.card": [
|
|
379
|
+
"CardPaymentProcessor"
|
|
380
|
+
],
|
|
367
381
|
"canvas_sdk.handlers.simple_api": [
|
|
368
382
|
"APIKeyAuthMixin",
|
|
369
383
|
"APIKeyCredentials",
|
plugin_runner/exceptions.py
CHANGED
|
@@ -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."""
|
plugin_runner/installation.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
77
|
-
"SELECT name, package, version, key, value
|
|
78
|
-
"
|
|
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
|
|
|
@@ -130,6 +147,8 @@ def download_plugin(plugin_package: str) -> Generator[Path, None, None]:
|
|
|
130
147
|
|
|
131
148
|
with open(download_path, "wb") as download_file:
|
|
132
149
|
response = requests.request(method=method, url=f"https://{host}{path}", headers=headers)
|
|
150
|
+
response.raise_for_status()
|
|
151
|
+
|
|
133
152
|
download_file.write(response.content)
|
|
134
153
|
|
|
135
154
|
yield download_path
|
|
@@ -151,7 +170,8 @@ def install_plugin(plugin_name: str, attributes: PluginAttributes) -> None:
|
|
|
151
170
|
|
|
152
171
|
install_plugin_secrets(plugin_name=plugin_name, secrets=attributes["secrets"])
|
|
153
172
|
except Exception as e:
|
|
154
|
-
log.error(f'Failed to install plugin "{plugin_name}", version {attributes["version"]}')
|
|
173
|
+
log.error(f'Failed to install plugin "{plugin_name}", version {attributes["version"]}: {e}')
|
|
174
|
+
|
|
155
175
|
sentry_sdk.capture_exception(e)
|
|
156
176
|
|
|
157
177
|
raise PluginInstallationError() from e
|
|
@@ -210,22 +230,31 @@ def disable_plugin(plugin_name: str) -> None:
|
|
|
210
230
|
|
|
211
231
|
def uninstall_plugin(plugin_name: str) -> None:
|
|
212
232
|
"""Remove the plugin from the filesystem."""
|
|
213
|
-
|
|
233
|
+
try:
|
|
234
|
+
log.info(f'Uninstalling plugin "{plugin_name}"')
|
|
214
235
|
|
|
215
|
-
|
|
236
|
+
plugin_path = Path(PLUGIN_DIRECTORY) / plugin_name
|
|
216
237
|
|
|
217
|
-
|
|
218
|
-
|
|
238
|
+
if plugin_path.exists():
|
|
239
|
+
shutil.rmtree(plugin_path)
|
|
240
|
+
except Exception as e:
|
|
241
|
+
raise PluginUninstallationError() from e
|
|
219
242
|
|
|
220
243
|
|
|
221
244
|
def install_plugins() -> None:
|
|
222
245
|
"""Install all enabled plugins."""
|
|
223
246
|
log.info("Installing plugins")
|
|
247
|
+
try:
|
|
248
|
+
plugins_dir = Path(PLUGIN_DIRECTORY).resolve()
|
|
224
249
|
|
|
225
|
-
|
|
226
|
-
|
|
250
|
+
if plugins_dir.exists():
|
|
251
|
+
shutil.rmtree(plugins_dir.as_posix())
|
|
227
252
|
|
|
228
|
-
|
|
253
|
+
plugins_dir.mkdir(parents=False, exist_ok=True)
|
|
254
|
+
except Exception as e:
|
|
255
|
+
raise PluginInstallationError(
|
|
256
|
+
f'Failed to reset plugin directory "{PLUGIN_DIRECTORY}": {e}"'
|
|
257
|
+
) from e
|
|
229
258
|
|
|
230
259
|
for plugin_name, attributes in enabled_plugins().items():
|
|
231
260
|
try:
|
plugin_runner/plugin_runner.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import base64
|
|
1
2
|
import json
|
|
2
3
|
import os
|
|
3
4
|
import pathlib
|
|
@@ -24,7 +25,14 @@ from sentry_sdk.integrations.logging import ignore_logger
|
|
|
24
25
|
|
|
25
26
|
import settings
|
|
26
27
|
from canvas_generated.messages.effects_pb2 import EffectType
|
|
27
|
-
from canvas_generated.messages.plugins_pb2 import
|
|
28
|
+
from canvas_generated.messages.plugins_pb2 import (
|
|
29
|
+
ReloadPluginRequest,
|
|
30
|
+
ReloadPluginResponse,
|
|
31
|
+
ReloadPluginsRequest,
|
|
32
|
+
ReloadPluginsResponse,
|
|
33
|
+
UnloadPluginRequest,
|
|
34
|
+
UnloadPluginResponse,
|
|
35
|
+
)
|
|
28
36
|
from canvas_generated.services.plugin_runner_pb2_grpc import (
|
|
29
37
|
PluginRunnerServicer,
|
|
30
38
|
add_PluginRunnerServicer_to_server,
|
|
@@ -38,7 +46,13 @@ from canvas_sdk.utils import metrics
|
|
|
38
46
|
from canvas_sdk.utils.metrics import measured
|
|
39
47
|
from logger import log
|
|
40
48
|
from plugin_runner.authentication import token_for_plugin
|
|
41
|
-
from plugin_runner.
|
|
49
|
+
from plugin_runner.exceptions import PluginInstallationError, PluginUninstallationError
|
|
50
|
+
from plugin_runner.installation import (
|
|
51
|
+
enabled_plugins,
|
|
52
|
+
install_plugin,
|
|
53
|
+
install_plugins,
|
|
54
|
+
uninstall_plugin,
|
|
55
|
+
)
|
|
42
56
|
from plugin_runner.sandbox import Sandbox, sandbox_from_module
|
|
43
57
|
from settings import (
|
|
44
58
|
CHANNEL_NAME,
|
|
@@ -200,6 +214,28 @@ class PluginRunner(PluginRunnerServicer):
|
|
|
200
214
|
# respond to SimpleAPI request events are not relevant
|
|
201
215
|
plugin_name = event.context["plugin_name"]
|
|
202
216
|
relevant_plugins = [p for p in relevant_plugins if p.startswith(f"{plugin_name}:")]
|
|
217
|
+
elif event_type in {
|
|
218
|
+
EventType.REVENUE__PAYMENT_PROCESSOR__CHARGE,
|
|
219
|
+
EventType.REVENUE__PAYMENT_PROCESSOR__SELECTED,
|
|
220
|
+
EventType.REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHODS__LIST,
|
|
221
|
+
EventType.REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHODS__ADD,
|
|
222
|
+
EventType.REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHODS__REMOVE,
|
|
223
|
+
}:
|
|
224
|
+
# The target plugin's name will be part of the payment processor identifier, so other plugins that
|
|
225
|
+
# respond to payment processor charge events are not relevant
|
|
226
|
+
try:
|
|
227
|
+
plugin_name = (
|
|
228
|
+
base64.b64decode(event.context["identifier"]).decode("utf-8").split(".")[0]
|
|
229
|
+
)
|
|
230
|
+
relevant_plugins = [
|
|
231
|
+
p for p in relevant_plugins if p.startswith(f"{plugin_name}:")
|
|
232
|
+
]
|
|
233
|
+
except Exception as ex:
|
|
234
|
+
log.error(
|
|
235
|
+
f"Failed to decode identifier for event {event_name} with context {event.context}"
|
|
236
|
+
)
|
|
237
|
+
sentry_sdk.capture_exception(ex)
|
|
238
|
+
relevant_plugins = []
|
|
203
239
|
|
|
204
240
|
effect_list = []
|
|
205
241
|
|
|
@@ -305,14 +341,51 @@ class PluginRunner(PluginRunnerServicer):
|
|
|
305
341
|
self, request: ReloadPluginsRequest, context: Any
|
|
306
342
|
) -> Iterable[ReloadPluginsResponse]:
|
|
307
343
|
"""This is invoked when we need to reload plugins."""
|
|
308
|
-
log.info("Reloading plugins...")
|
|
344
|
+
log.info("Reloading all plugins...")
|
|
345
|
+
|
|
346
|
+
message = {"action": "reload"}
|
|
347
|
+
|
|
309
348
|
try:
|
|
310
|
-
publish_message(message=
|
|
349
|
+
publish_message(message=message)
|
|
311
350
|
except ImportError:
|
|
312
351
|
yield ReloadPluginsResponse(success=False)
|
|
313
352
|
else:
|
|
314
353
|
yield ReloadPluginsResponse(success=True)
|
|
315
354
|
|
|
355
|
+
def ReloadPlugin(
|
|
356
|
+
self, request: ReloadPluginRequest, context: Any
|
|
357
|
+
) -> Iterable[ReloadPluginResponse]:
|
|
358
|
+
"""This is invoked when we need to reload a specific plugin."""
|
|
359
|
+
log.info(f'Reloading plugin "{request.plugin}"...')
|
|
360
|
+
|
|
361
|
+
message = {
|
|
362
|
+
"action": "reload",
|
|
363
|
+
"plugin": request.plugin,
|
|
364
|
+
}
|
|
365
|
+
try:
|
|
366
|
+
publish_message(message=message)
|
|
367
|
+
except ImportError:
|
|
368
|
+
yield ReloadPluginResponse(success=False)
|
|
369
|
+
else:
|
|
370
|
+
yield ReloadPluginResponse(success=True)
|
|
371
|
+
|
|
372
|
+
def UnloadPlugin(
|
|
373
|
+
self, request: UnloadPluginRequest, context: Any
|
|
374
|
+
) -> Iterable[UnloadPluginResponse]:
|
|
375
|
+
"""This is invoked when we need to reload a specific plugin."""
|
|
376
|
+
log.info(f'Unloading plugin "{request.plugin}"...')
|
|
377
|
+
|
|
378
|
+
message = {
|
|
379
|
+
"action": "unload",
|
|
380
|
+
"plugin": request.plugin,
|
|
381
|
+
}
|
|
382
|
+
try:
|
|
383
|
+
publish_message(message=message)
|
|
384
|
+
except ImportError:
|
|
385
|
+
yield UnloadPluginResponse(success=False)
|
|
386
|
+
else:
|
|
387
|
+
yield UnloadPluginResponse(success=True)
|
|
388
|
+
|
|
316
389
|
|
|
317
390
|
STOP_SYNCHRONIZER = threading.Event()
|
|
318
391
|
|
|
@@ -346,20 +419,40 @@ def synchronize_plugins(run_once: bool = False) -> None:
|
|
|
346
419
|
if "action" not in data:
|
|
347
420
|
continue
|
|
348
421
|
|
|
349
|
-
|
|
350
|
-
|
|
422
|
+
plugin_name = data.get("plugin", None)
|
|
423
|
+
try:
|
|
424
|
+
if data["action"] == "reload":
|
|
425
|
+
if plugin_name:
|
|
426
|
+
plugin = enabled_plugins([plugin_name]).get(plugin_name, None)
|
|
427
|
+
|
|
428
|
+
if plugin:
|
|
429
|
+
log.info(
|
|
430
|
+
f'synchronize_plugins: installing/reloading plugin "{plugin_name}" for action=reload'
|
|
431
|
+
)
|
|
432
|
+
install_plugin(plugin_name, attributes=plugin)
|
|
433
|
+
plugin_dir = pathlib.Path(PLUGIN_DIRECTORY) / plugin_name
|
|
434
|
+
load_plugin(plugin_dir.resolve())
|
|
435
|
+
else:
|
|
436
|
+
log.info("synchronize_plugins: installing/reloading plugins for action=reload")
|
|
437
|
+
install_plugins()
|
|
438
|
+
load_plugins()
|
|
439
|
+
elif data["action"] == "unload" and plugin_name:
|
|
440
|
+
log.info(f'synchronize_plugins: uninstalling plugin "{plugin_name}"')
|
|
441
|
+
unload_plugin(plugin_name)
|
|
442
|
+
uninstall_plugin(plugin_name)
|
|
443
|
+
except Exception as e:
|
|
444
|
+
if isinstance(e, PluginInstallationError):
|
|
445
|
+
message = "install_plugins failed"
|
|
446
|
+
elif isinstance(e, PluginUninstallationError):
|
|
447
|
+
message = "uninstall_plugin failed"
|
|
448
|
+
else:
|
|
449
|
+
message = "load_plugins failed"
|
|
351
450
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
except Exception as e:
|
|
355
|
-
log.error(f"synchronize_plugins: install_plugins failed: {e}")
|
|
356
|
-
sentry_sdk.capture_exception(e)
|
|
451
|
+
if plugin_name:
|
|
452
|
+
message += f' for plugin "{plugin_name}"'
|
|
357
453
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
except Exception as e:
|
|
361
|
-
log.error(f"synchronize_plugins: load_plugins failed: {e}")
|
|
362
|
-
sentry_sdk.capture_exception(e)
|
|
454
|
+
log.error(f"synchronize_plugins: {message}: {e}")
|
|
455
|
+
sentry_sdk.capture_exception(e)
|
|
363
456
|
|
|
364
457
|
if run_once:
|
|
365
458
|
break
|
|
@@ -523,7 +616,7 @@ def load_or_reload_plugin(path: pathlib.Path) -> bool:
|
|
|
523
616
|
result = sandbox.execute()
|
|
524
617
|
|
|
525
618
|
if name_and_class in LOADED_PLUGINS:
|
|
526
|
-
log.info(f"Reloading
|
|
619
|
+
log.info(f"Reloading handler '{name_and_class}'")
|
|
527
620
|
|
|
528
621
|
LOADED_PLUGINS[name_and_class]["active"] = True
|
|
529
622
|
|
|
@@ -531,7 +624,7 @@ def load_or_reload_plugin(path: pathlib.Path) -> bool:
|
|
|
531
624
|
LOADED_PLUGINS[name_and_class]["sandbox"] = result
|
|
532
625
|
LOADED_PLUGINS[name_and_class]["secrets"] = secrets_json
|
|
533
626
|
else:
|
|
534
|
-
log.info(f
|
|
627
|
+
log.info(f'Loading handler "{name_and_class}"')
|
|
535
628
|
|
|
536
629
|
LOADED_PLUGINS[name_and_class] = {
|
|
537
630
|
"active": True,
|
|
@@ -553,6 +646,23 @@ def load_or_reload_plugin(path: pathlib.Path) -> bool:
|
|
|
553
646
|
return not any_failed
|
|
554
647
|
|
|
555
648
|
|
|
649
|
+
def unload_plugin(name: str) -> None:
|
|
650
|
+
"""Unload a plugin by its name."""
|
|
651
|
+
handlers_removed = False
|
|
652
|
+
|
|
653
|
+
for handler_name in LOADED_PLUGINS.copy():
|
|
654
|
+
if handler_name.startswith(f"{name}:"):
|
|
655
|
+
log.info(f'Unloading handler "{handler_name}"')
|
|
656
|
+
del LOADED_PLUGINS[handler_name]
|
|
657
|
+
handlers_removed = True
|
|
658
|
+
|
|
659
|
+
if handlers_removed:
|
|
660
|
+
# Refresh the event type map to remove any handlers for the unloaded plugin
|
|
661
|
+
refresh_event_type_map()
|
|
662
|
+
else:
|
|
663
|
+
log.warning(f"No handlers found for plugin '{name}' to unload.")
|
|
664
|
+
|
|
665
|
+
|
|
556
666
|
def refresh_event_type_map() -> None:
|
|
557
667
|
"""Ensure the event subscriptions are up to date."""
|
|
558
668
|
EVENT_HANDLER_MAP.clear()
|
|
@@ -608,6 +718,13 @@ def load_plugins(specified_plugin_paths: list[str] | None = None) -> None:
|
|
|
608
718
|
refresh_event_type_map()
|
|
609
719
|
|
|
610
720
|
|
|
721
|
+
@measured
|
|
722
|
+
def load_plugin(path: pathlib.Path) -> None:
|
|
723
|
+
"""Load a plugin from the specified path."""
|
|
724
|
+
load_or_reload_plugin(path)
|
|
725
|
+
refresh_event_type_map()
|
|
726
|
+
|
|
727
|
+
|
|
611
728
|
# NOTE: specified_plugin_paths powers the `canvas run-plugins` command
|
|
612
729
|
def main(specified_plugin_paths: list[str] | None = None) -> None:
|
|
613
730
|
"""Run the server and the synchronize_plugins loop."""
|
plugin_runner/sandbox.py
CHANGED
|
@@ -303,6 +303,14 @@ enum EffectType {
|
|
|
303
303
|
|
|
304
304
|
CREATE_COMPOUND_MEDICATION = 6020;
|
|
305
305
|
UPDATE_COMPOUND_MEDICATION = 6021;
|
|
306
|
+
|
|
307
|
+
REVENUE__PAYMENT_PROCESSOR__METADATA = 7001;
|
|
308
|
+
REVENUE__PAYMENT_PROCESSOR__FORM = 7002;
|
|
309
|
+
REVENUE__PAYMENT_PROCESSOR__CREDIT_CARD_TRANSACTION = 7003;
|
|
310
|
+
REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHOD = 7004;
|
|
311
|
+
REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHOD__ADD_RESPONSE = 7005;
|
|
312
|
+
REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHOD__REMOVE_RESPONSE = 7006;
|
|
313
|
+
|
|
306
314
|
}
|
|
307
315
|
|
|
308
316
|
message Effect {
|
|
@@ -1160,6 +1160,14 @@ enum EventType {
|
|
|
1160
1160
|
DOCUMENT_REFERENCE_CREATED = 150000;
|
|
1161
1161
|
DOCUMENT_REFERENCE_UPDATED = 150001;
|
|
1162
1162
|
DOCUMENT_REFERENCE_DELETED = 150002;
|
|
1163
|
+
|
|
1164
|
+
|
|
1165
|
+
REVENUE__PAYMENT_PROCESSOR__LIST = 160001;
|
|
1166
|
+
REVENUE__PAYMENT_PROCESSOR__CHARGE = 160003;
|
|
1167
|
+
REVENUE__PAYMENT_PROCESSOR__SELECTED = 160002;
|
|
1168
|
+
REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHODS__LIST = 160004;
|
|
1169
|
+
REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHODS__ADD = 160005;
|
|
1170
|
+
REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHODS__REMOVE = 160006;
|
|
1163
1171
|
}
|
|
1164
1172
|
|
|
1165
1173
|
message Event {
|
|
@@ -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
|
}
|
|
File without changes
|
|
File without changes
|