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.
- {canvas-0.24.0.dist-info → canvas-0.25.0.dist-info}/METADATA +1 -1
- {canvas-0.24.0.dist-info → canvas-0.25.0.dist-info}/RECORD +26 -14
- canvas_generated/messages/effects_pb2.py +2 -2
- canvas_generated/messages/effects_pb2.pyi +2 -0
- canvas_generated/messages/events_pb2.py +2 -2
- canvas_generated/messages/events_pb2.pyi +4 -0
- canvas_sdk/effects/simple_api.py +83 -0
- canvas_sdk/handlers/base.py +4 -0
- canvas_sdk/handlers/simple_api/__init__.py +22 -0
- canvas_sdk/handlers/simple_api/api.py +328 -0
- canvas_sdk/handlers/simple_api/exceptions.py +39 -0
- canvas_sdk/handlers/simple_api/security.py +184 -0
- canvas_sdk/tests/handlers/__init__.py +0 -0
- canvas_sdk/tests/handlers/test_simple_api.py +828 -0
- plugin_runner/plugin_runner.py +27 -0
- plugin_runner/sandbox.py +1 -0
- plugin_runner/tests/fixtures/plugins/test_simple_api/CANVAS_MANIFEST.json +47 -0
- plugin_runner/tests/fixtures/plugins/test_simple_api/README.md +11 -0
- plugin_runner/tests/fixtures/plugins/test_simple_api/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_simple_api/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_simple_api/protocols/my_protocol.py +43 -0
- plugin_runner/tests/test_plugin_runner.py +71 -1
- protobufs/canvas_generated/messages/effects.proto +2 -0
- protobufs/canvas_generated/messages/events.proto +4 -0
- {canvas-0.24.0.dist-info → canvas-0.25.0.dist-info}/WHEEL +0 -0
- {canvas-0.24.0.dist-info → canvas-0.25.0.dist-info}/entry_points.txt +0 -0
plugin_runner/plugin_runner.py
CHANGED
|
@@ -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
|
@@ -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
|
+
}
|
|
File without changes
|
|
File without changes
|
|
@@ -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]
|
|
@@ -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 {
|
|
File without changes
|
|
File without changes
|