canvas 0.24.0__py3-none-any.whl → 0.25.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.

@@ -9,6 +9,7 @@ import time
9
9
  import traceback
10
10
  from collections import defaultdict
11
11
  from collections.abc import AsyncGenerator
12
+ from http import HTTPStatus
12
13
  from typing import Any, TypedDict
13
14
 
14
15
  import grpc
@@ -23,6 +24,7 @@ from canvas_generated.services.plugin_runner_pb2_grpc import (
23
24
  add_PluginRunnerServicer_to_server,
24
25
  )
25
26
  from canvas_sdk.effects import Effect
27
+ from canvas_sdk.effects.simple_api import Response
26
28
  from canvas_sdk.events import Event, EventRequest, EventResponse, EventType
27
29
  from canvas_sdk.protocols import ClinicalQualityMeasure
28
30
  from canvas_sdk.utils.stats import get_duration_ms, statsd_client
@@ -139,6 +141,7 @@ class PluginRunner(PluginRunnerServicer):
139
141
  event_type = event.type
140
142
  event_name = event.name
141
143
  relevant_plugins = EVENT_HANDLER_MAP[event_name]
144
+ relevant_plugin_handlers = []
142
145
 
143
146
  log.debug(f"Processing {relevant_plugins} for {event_name}")
144
147
 
@@ -149,6 +152,11 @@ class PluginRunner(PluginRunnerServicer):
149
152
  plugin_name = event.target.id
150
153
  # filter only for the plugin(s) that were created/updated
151
154
  relevant_plugins = [p for p in relevant_plugins if p.startswith(f"{plugin_name}:")]
155
+ elif event_type in {EventType.SIMPLE_API_AUTHENTICATE, EventType.SIMPLE_API_REQUEST}:
156
+ # The target plugin's name will be part of the URL path, so other plugins that respond
157
+ # to SimpleAPI request events are not relevant
158
+ plugin_name = event.context["plugin_name"]
159
+ relevant_plugins = [p for p in relevant_plugins if p.startswith(f"{plugin_name}:")]
152
160
 
153
161
  effect_list = []
154
162
 
@@ -164,6 +172,11 @@ class PluginRunner(PluginRunnerServicer):
164
172
 
165
173
  try:
166
174
  handler = handler_class(event, secrets)
175
+
176
+ if not handler.accept_event():
177
+ continue
178
+ relevant_plugin_handlers.append(handler_class)
179
+
167
180
  classname = (
168
181
  handler.__class__.__name__
169
182
  if isinstance(handler, ClinicalQualityMeasure)
@@ -205,6 +218,20 @@ class PluginRunner(PluginRunnerServicer):
205
218
 
206
219
  effect_list += effects
207
220
 
221
+ # Special handling for SimpleAPI requests: if there were no relevant handlers (as determined
222
+ # by calling ignore_event on handlers), then set the effects list to be a single 404 Not
223
+ # Found response effect. If multiple handlers were able to respond, log an error and set the
224
+ # effects list to be a single 500 Internal Server Error response effect.
225
+ if event.type in {EventType.SIMPLE_API_AUTHENTICATE, EventType.SIMPLE_API_REQUEST}:
226
+ if len(relevant_plugin_handlers) == 0:
227
+ effect_list = [Response(status_code=HTTPStatus.NOT_FOUND).apply()]
228
+ elif len(relevant_plugin_handlers) > 1:
229
+ log.error(
230
+ f"Multiple handlers responded to {EventType.Name(EventType.SIMPLE_API_REQUEST)}"
231
+ f" {event.context['path']}"
232
+ )
233
+ effect_list = [Response(status_code=HTTPStatus.INTERNAL_SERVER_ERROR).apply()]
234
+
208
235
  event_duration = get_duration_ms(event_start_time)
209
236
 
210
237
  # Don't log anything if a plugin handler didn't actually run.
plugin_runner/sandbox.py CHANGED
@@ -73,6 +73,7 @@ ALLOWED_MODULES = frozenset(
73
73
  "rapidfuzz",
74
74
  "re",
75
75
  "requests",
76
+ "secrets",
76
77
  "string",
77
78
  "time",
78
79
  "traceback",
@@ -0,0 +1,47 @@
1
+ {
2
+ "sdk_version": "0.1.4",
3
+ "plugin_version": "0.0.1",
4
+ "name": "test_simple_api",
5
+ "description": "Edit the description in CANVAS_MANIFEST.json",
6
+ "components": {
7
+ "protocols": [
8
+ {
9
+ "class": "test_simple_api.protocols.my_protocol:Route",
10
+ "description": "A protocol that does xyz...",
11
+ "data_access": {
12
+ "event": "",
13
+ "read": [],
14
+ "write": []
15
+ }
16
+ },
17
+ {
18
+ "class": "test_simple_api.protocols.my_protocol:ErrorRoute1",
19
+ "description": "A protocol that does xyz...",
20
+ "data_access": {
21
+ "event": "",
22
+ "read": [],
23
+ "write": []
24
+ }
25
+ },
26
+ {
27
+ "class": "test_simple_api.protocols.my_protocol:ErrorRoute2",
28
+ "description": "A protocol that does xyz...",
29
+ "data_access": {
30
+ "event": "",
31
+ "read": [],
32
+ "write": []
33
+ }
34
+ }
35
+ ],
36
+ "commands": [],
37
+ "content": [],
38
+ "effects": [],
39
+ "views": []
40
+ },
41
+ "secrets": [],
42
+ "tags": {},
43
+ "references": [],
44
+ "license": "",
45
+ "diagram": false,
46
+ "readme": "./README.md"
47
+ }
@@ -0,0 +1,11 @@
1
+ test_simple_api
2
+ ===============
3
+
4
+ ## Description
5
+
6
+ A description of this plugin
7
+
8
+ ### Important Note!
9
+
10
+ The CANVAS_MANIFEST.json is used when installing your plugin. Please ensure it
11
+ gets updated if you add, remove, or rename protocols.
@@ -0,0 +1,43 @@
1
+ from http import HTTPStatus
2
+
3
+ from canvas_sdk.effects import Effect
4
+ from canvas_sdk.effects.simple_api import Response
5
+ from canvas_sdk.handlers.simple_api import Credentials, SimpleAPIRoute
6
+
7
+
8
+ class NoAuth(SimpleAPIRoute):
9
+ """SimpleAPIRoute base class to bypass authentication."""
10
+
11
+ def authenticate(self, credentials: Credentials) -> bool:
12
+ """Authenticate the request."""
13
+ return True
14
+
15
+
16
+ class Route(NoAuth):
17
+ """Handler for /route."""
18
+
19
+ PATH = "/route"
20
+
21
+ def get(self) -> list[Response | Effect]:
22
+ """Handler method for GET."""
23
+ return [Response(status_code=HTTPStatus.OK)]
24
+
25
+
26
+ class ErrorRoute1(NoAuth):
27
+ """Handler #1 for /error."""
28
+
29
+ PATH = "/error"
30
+
31
+ def get(self) -> list[Response | Effect]:
32
+ """Handler method for GET."""
33
+ return [Response(status_code=HTTPStatus.OK)]
34
+
35
+
36
+ class ErrorRoute2(NoAuth):
37
+ """Handler #2 for /error."""
38
+
39
+ PATH = "/error"
40
+
41
+ def get(self) -> list[Response | Effect]:
42
+ """Handler method for GET."""
43
+ return [Response(status_code=HTTPStatus.OK)]
@@ -1,14 +1,19 @@
1
1
  import asyncio
2
+ import json
2
3
  import logging
3
4
  import pickle
4
5
  import shutil
6
+ from base64 import b64encode
7
+ from http import HTTPStatus
5
8
  from pathlib import Path
9
+ from typing import Any
6
10
  from unittest.mock import AsyncMock, MagicMock, patch
7
11
 
8
12
  import pytest
9
13
 
10
14
  from canvas_generated.messages.effects_pb2 import EffectType
11
15
  from canvas_generated.messages.plugins_pb2 import ReloadPluginsRequest
16
+ from canvas_sdk.effects.simple_api import Response
12
17
  from canvas_sdk.events import Event, EventRequest, EventType
13
18
  from plugin_runner.plugin_runner import (
14
19
  EVENT_HANDLER_MAP,
@@ -24,7 +29,7 @@ from plugin_runner.plugin_runner import (
24
29
  def plugin_runner() -> PluginRunner:
25
30
  """Fixture to initialize PluginRunner with mocks."""
26
31
  runner = PluginRunner()
27
- runner.statsd_client = MagicMock()
32
+ runner.statsd_client = MagicMock() # type: ignore[attr-defined]
28
33
  return runner
29
34
 
30
35
 
@@ -316,3 +321,68 @@ def import_me() -> str:
316
321
  assert len(result[0].effects) == 1
317
322
  assert result[0].effects[0].type == EffectType.LOG
318
323
  assert result[0].effects[0].payload == "Successfully changed!"
324
+
325
+
326
+ @pytest.mark.asyncio
327
+ @pytest.mark.parametrize(
328
+ argnames="context,status_code",
329
+ argvalues=[
330
+ (
331
+ {
332
+ "plugin_name": "test_simple_api",
333
+ "method": "GET",
334
+ "path": "/route",
335
+ "query_string": "",
336
+ "body": b64encode(b"").decode(),
337
+ "headers": {},
338
+ },
339
+ HTTPStatus.OK,
340
+ ),
341
+ (
342
+ {
343
+ "plugin_name": "test_simple_api",
344
+ "method": "GET",
345
+ "path": "/notfound",
346
+ "query_string": "",
347
+ "body": b64encode(b"").decode(),
348
+ "headers": {},
349
+ },
350
+ HTTPStatus.NOT_FOUND,
351
+ ),
352
+ (
353
+ {
354
+ "plugin_name": "test_simple_api",
355
+ "method": "GET",
356
+ "path": "/error",
357
+ "query_string": "",
358
+ "body": b64encode(b"").decode(),
359
+ "headers": {},
360
+ },
361
+ HTTPStatus.INTERNAL_SERVER_ERROR,
362
+ ),
363
+ ],
364
+ ids=["success", "not found error", "multiple handlers error"],
365
+ )
366
+ @pytest.mark.parametrize("install_test_plugin", ["test_simple_api"], indirect=True)
367
+ async def test_simple_api(
368
+ install_test_plugin: Path,
369
+ load_test_plugins: None,
370
+ plugin_runner: PluginRunner,
371
+ context: dict[str, Any],
372
+ status_code: HTTPStatus,
373
+ ) -> None:
374
+ """Test that the PluginRunner returns responses to SimpleAPI request events."""
375
+ event = EventRequest(
376
+ type=EventType.SIMPLE_API_REQUEST,
377
+ context=json.dumps(context),
378
+ )
379
+
380
+ result = []
381
+ async for response in plugin_runner.HandleEvent(event, None):
382
+ result.append(response)
383
+
384
+ expected_response = Response(status_code=status_code).apply()
385
+ if status_code == HTTPStatus.OK:
386
+ expected_response.plugin_name = "test_simple_api"
387
+
388
+ assert result[0].effects == [expected_response]
@@ -249,6 +249,8 @@ enum EffectType {
249
249
  PORTAL_WIDGET = 2101;
250
250
 
251
251
  LAUNCH_MODAL = 3000;
252
+
253
+ SIMPLE_API_RESPONSE = 4000;
252
254
  }
253
255
 
254
256
  message Effect {
@@ -254,6 +254,7 @@ enum EventType {
254
254
  // CHART_SECTION_REVIEW_COMMAND__PRE_EXECUTE_ACTION = 9010;
255
255
  // CHART_SECTION_REVIEW_COMMAND__POST_EXECUTE_ACTION = 9011;
256
256
 
257
+
257
258
  CLIPBOARD_COMMAND__PRE_ORIGINATE = 53000;
258
259
  CLIPBOARD_COMMAND__POST_ORIGINATE = 53001;
259
260
  CLIPBOARD_COMMAND__PRE_UPDATE = 53002;
@@ -1064,6 +1065,9 @@ enum EventType {
1064
1065
  SHOW_CHART_SUMMARY_SURGICAL_HISTORY_SECTION_BUTTON = 120008;
1065
1066
  SHOW_CHART_SUMMARY_FAMILY_HISTORY_SECTION_BUTTON = 120009;
1066
1067
  SHOW_CHART_SUMMARY_CODING_GAPS_SECTION_BUTTON = 120010;
1068
+
1069
+ SIMPLE_API_AUTHENTICATE = 130000;
1070
+ SIMPLE_API_REQUEST = 130001;
1067
1071
  }
1068
1072
 
1069
1073
  message Event {