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.

Files changed (32) hide show
  1. {canvas-0.53.1.dist-info → canvas-0.54.0.dist-info}/METADATA +1 -1
  2. {canvas-0.53.1.dist-info → canvas-0.54.0.dist-info}/RECORD +32 -28
  3. canvas_cli/apps/plugin/plugin.py +40 -8
  4. canvas_generated/messages/effects_pb2.py +2 -2
  5. canvas_generated/messages/effects_pb2.pyi +12 -0
  6. canvas_generated/messages/events_pb2.py +2 -2
  7. canvas_generated/messages/events_pb2.pyi +12 -0
  8. canvas_generated/messages/plugins_pb2.py +9 -1
  9. canvas_generated/messages/plugins_pb2.pyi +24 -0
  10. canvas_generated/services/plugin_runner_pb2.py +2 -2
  11. canvas_generated/services/plugin_runner_pb2_grpc.py +66 -0
  12. canvas_sdk/caching/client.py +3 -1
  13. canvas_sdk/caching/plugins.py +7 -4
  14. canvas_sdk/effects/payment_processor.py +137 -0
  15. canvas_sdk/handlers/payment_processors/__init__.py +1 -0
  16. canvas_sdk/handlers/payment_processors/base.py +93 -0
  17. canvas_sdk/handlers/payment_processors/card.py +200 -0
  18. canvas_sdk/questionnaires/utils.py +2 -2
  19. canvas_sdk/templates/utils.py +5 -3
  20. canvas_sdk/utils/plugins.py +33 -20
  21. canvas_sdk/v1/data/patient.py +7 -0
  22. plugin_runner/allowed-module-imports.json +14 -0
  23. plugin_runner/exceptions.py +4 -0
  24. plugin_runner/installation.py +43 -14
  25. plugin_runner/plugin_runner.py +135 -18
  26. plugin_runner/sandbox.py +1 -0
  27. protobufs/canvas_generated/messages/effects.proto +8 -0
  28. protobufs/canvas_generated/messages/events.proto +8 -0
  29. protobufs/canvas_generated/messages/plugins.proto +16 -1
  30. protobufs/canvas_generated/services/plugin_runner.proto +4 -0
  31. {canvas-0.53.1.dist-info → canvas-0.54.0.dist-info}/WHEEL +0 -0
  32. {canvas-0.53.1.dist-info → canvas-0.54.0.dist-info}/entry_points.txt +0 -0
@@ -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 plugin_only(func: Callable[..., Any]) -> Callable[..., Any]:
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
- current_frame = inspect.currentframe()
17
- caller = current_frame.f_back if current_frame else None
34
+ plugin_frame = find_plugin_ancestor(inspect.currentframe())
18
35
 
19
- if not caller or "__is_plugin__" not in caller.f_globals:
20
- return None
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 = caller.f_globals["__name__"].split(".")[0]
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(depth: int = 10, frame: FrameType | None = None) -> tuple[bool, str | None]:
53
+ def is_plugin_caller() -> tuple[bool, str | None]:
34
54
  """Check if a function is called from a plugin."""
35
- current_frame = frame or inspect.currentframe()
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 "__is_plugin__" not in caller.f_globals:
42
- if depth > 0:
43
- return is_plugin_caller(frame=caller, depth=depth - 1)
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
- module = caller.f_globals.get("__name__")
48
- qualname = caller.f_code.co_qualname
61
+ return True, f"{module}.{qualname}"
49
62
 
50
- return True, f"{module}.{qualname}"
63
+ return False, None
51
64
 
52
65
 
53
66
  __exports__ = ()
@@ -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",
@@ -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
 
@@ -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
- log.info(f'Uninstalling plugin "{plugin_name}"')
233
+ try:
234
+ log.info(f'Uninstalling plugin "{plugin_name}"')
214
235
 
215
- plugin_path = Path(PLUGIN_DIRECTORY) / plugin_name
236
+ plugin_path = Path(PLUGIN_DIRECTORY) / plugin_name
216
237
 
217
- if plugin_path.exists():
218
- shutil.rmtree(plugin_path)
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
- if Path(PLUGIN_DIRECTORY).exists():
226
- shutil.rmtree(PLUGIN_DIRECTORY)
250
+ if plugins_dir.exists():
251
+ shutil.rmtree(plugins_dir.as_posix())
227
252
 
228
- os.mkdir(PLUGIN_DIRECTORY)
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:
@@ -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 ReloadPluginsRequest, ReloadPluginsResponse
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.installation import install_plugins
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={"action": "reload"})
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
- if data["action"] == "reload":
350
- log.info("synchronize_plugins: installing/reloading plugins for action=reload")
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
- 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)
451
+ if plugin_name:
452
+ message += f' for plugin "{plugin_name}"'
357
453
 
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)
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 plugin '{name_and_class}'")
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"Loading plugin '{name_and_class}'")
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
@@ -225,6 +225,7 @@ THIRD_PARTY_MODULES = {
225
225
  },
226
226
  "pydantic": {
227
227
  "BaseModel",
228
+ "ConfigDict",
228
229
  "conint",
229
230
  "constr",
230
231
  "Field",
@@ -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
  }