canvas 0.16.0__py3-none-any.whl → 0.18.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.16.0.dist-info → canvas-0.18.0.dist-info}/METADATA +25 -31
- {canvas-0.16.0.dist-info → canvas-0.18.0.dist-info}/RECORD +40 -34
- {canvas-0.16.0.dist-info → canvas-0.18.0.dist-info}/WHEEL +1 -1
- canvas-0.18.0.dist-info/entry_points.txt +2 -0
- canvas_cli/apps/plugin/plugin.py +5 -1
- canvas_generated/messages/events_pb2.py +2 -2
- canvas_generated/messages/events_pb2.pyi +88 -0
- canvas_sdk/commands/tests/protocol/tests.py +12 -2
- canvas_sdk/commands/tests/test_utils.py +4 -9
- canvas_sdk/effects/banner_alert/tests.py +5 -2
- canvas_sdk/effects/surescripts/__init__.py +11 -0
- canvas_sdk/effects/task/__init__.py +3 -0
- canvas_sdk/effects/task/task.py +3 -0
- canvas_sdk/utils/__init__.py +8 -2
- canvas_sdk/utils/http.py +113 -3
- canvas_sdk/v1/data/team.py +76 -0
- plugin_runner/plugin_installer.py +21 -13
- plugin_runner/plugin_runner.py +67 -14
- plugin_runner/tests/test_plugin_runner.py +37 -5
- protobufs/canvas_generated/messages/effects.proto +225 -0
- protobufs/canvas_generated/messages/events.proto +1057 -0
- protobufs/canvas_generated/messages/plugins.proto +9 -0
- protobufs/canvas_generated/services/plugin_runner.proto +12 -0
- settings.py +4 -1
- canvas-0.16.0.dist-info/entry_points.txt +0 -3
- plugin_runner/plugin_synchronizer.py +0 -92
|
@@ -136,6 +136,20 @@ class EventType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
|
|
136
136
|
ASSESS_COMMAND__POST_EXECUTE_ACTION: _ClassVar[EventType]
|
|
137
137
|
ASSESS__CONDITION__POST_SEARCH: _ClassVar[EventType]
|
|
138
138
|
ASSESS__CONDITION__PRE_SEARCH: _ClassVar[EventType]
|
|
139
|
+
CANCEL_PRESCRIPTION_COMMAND__PRE_ORIGINATE: _ClassVar[EventType]
|
|
140
|
+
CANCEL_PRESCRIPTION_COMMAND__POST_ORIGINATE: _ClassVar[EventType]
|
|
141
|
+
CANCEL_PRESCRIPTION_COMMAND__PRE_UPDATE: _ClassVar[EventType]
|
|
142
|
+
CANCEL_PRESCRIPTION_COMMAND__POST_UPDATE: _ClassVar[EventType]
|
|
143
|
+
CANCEL_PRESCRIPTION_COMMAND__PRE_COMMIT: _ClassVar[EventType]
|
|
144
|
+
CANCEL_PRESCRIPTION_COMMAND__POST_COMMIT: _ClassVar[EventType]
|
|
145
|
+
CANCEL_PRESCRIPTION_COMMAND__PRE_DELETE: _ClassVar[EventType]
|
|
146
|
+
CANCEL_PRESCRIPTION_COMMAND__POST_DELETE: _ClassVar[EventType]
|
|
147
|
+
CANCEL_PRESCRIPTION_COMMAND__PRE_ENTER_IN_ERROR: _ClassVar[EventType]
|
|
148
|
+
CANCEL_PRESCRIPTION_COMMAND__POST_ENTER_IN_ERROR: _ClassVar[EventType]
|
|
149
|
+
CANCEL_PRESCRIPTION_COMMAND__PRE_EXECUTE_ACTION: _ClassVar[EventType]
|
|
150
|
+
CANCEL_PRESCRIPTION_COMMAND__POST_EXECUTE_ACTION: _ClassVar[EventType]
|
|
151
|
+
CANCEL_PRESCRIPTION__SELECTED_PRESCRIPTION__PRE_SEARCH: _ClassVar[EventType]
|
|
152
|
+
CANCEL_PRESCRIPTION__SELECTED_PRESCRIPTION__POST_SEARCH: _ClassVar[EventType]
|
|
139
153
|
CLIPBOARD_COMMAND__PRE_ORIGINATE: _ClassVar[EventType]
|
|
140
154
|
CLIPBOARD_COMMAND__POST_ORIGINATE: _ClassVar[EventType]
|
|
141
155
|
CLIPBOARD_COMMAND__PRE_UPDATE: _ClassVar[EventType]
|
|
@@ -503,6 +517,20 @@ class EventType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
|
|
503
517
|
ROS_COMMAND__POST_INSERTED_INTO_NOTE: _ClassVar[EventType]
|
|
504
518
|
ROS__QUESTIONNAIRE__PRE_SEARCH: _ClassVar[EventType]
|
|
505
519
|
ROS__QUESTIONNAIRE__POST_SEARCH: _ClassVar[EventType]
|
|
520
|
+
SNOOZE_PROTOCOL_COMMAND__PRE_ORIGINATE: _ClassVar[EventType]
|
|
521
|
+
SNOOZE_PROTOCOL_COMMAND__POST_ORIGINATE: _ClassVar[EventType]
|
|
522
|
+
SNOOZE_PROTOCOL_COMMAND__PRE_UPDATE: _ClassVar[EventType]
|
|
523
|
+
SNOOZE_PROTOCOL_COMMAND__POST_UPDATE: _ClassVar[EventType]
|
|
524
|
+
SNOOZE_PROTOCOL_COMMAND__PRE_COMMIT: _ClassVar[EventType]
|
|
525
|
+
SNOOZE_PROTOCOL_COMMAND__POST_COMMIT: _ClassVar[EventType]
|
|
526
|
+
SNOOZE_PROTOCOL_COMMAND__PRE_DELETE: _ClassVar[EventType]
|
|
527
|
+
SNOOZE_PROTOCOL_COMMAND__POST_DELETE: _ClassVar[EventType]
|
|
528
|
+
SNOOZE_PROTOCOL_COMMAND__PRE_ENTER_IN_ERROR: _ClassVar[EventType]
|
|
529
|
+
SNOOZE_PROTOCOL_COMMAND__POST_ENTER_IN_ERROR: _ClassVar[EventType]
|
|
530
|
+
SNOOZE_PROTOCOL_COMMAND__PRE_EXECUTE_ACTION: _ClassVar[EventType]
|
|
531
|
+
SNOOZE_PROTOCOL_COMMAND__POST_EXECUTE_ACTION: _ClassVar[EventType]
|
|
532
|
+
SNOOZE_PROTOCOL__PROTOCOL__PRE_SEARCH: _ClassVar[EventType]
|
|
533
|
+
SNOOZE_PROTOCOL__PROTOCOL__POST_SEARCH: _ClassVar[EventType]
|
|
506
534
|
STOP_MEDICATION_COMMAND__PRE_ORIGINATE: _ClassVar[EventType]
|
|
507
535
|
STOP_MEDICATION_COMMAND__POST_ORIGINATE: _ClassVar[EventType]
|
|
508
536
|
STOP_MEDICATION_COMMAND__PRE_UPDATE: _ClassVar[EventType]
|
|
@@ -565,6 +593,22 @@ class EventType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
|
|
565
593
|
TASK__ASSIGN_TO__POST_SEARCH: _ClassVar[EventType]
|
|
566
594
|
TASK__LABELS__PRE_SEARCH: _ClassVar[EventType]
|
|
567
595
|
TASK__LABELS__POST_SEARCH: _ClassVar[EventType]
|
|
596
|
+
UPDATE_DIAGNOSIS_COMMAND__PRE_ORIGINATE: _ClassVar[EventType]
|
|
597
|
+
UPDATE_DIAGNOSIS_COMMAND__POST_ORIGINATE: _ClassVar[EventType]
|
|
598
|
+
UPDATE_DIAGNOSIS_COMMAND__PRE_UPDATE: _ClassVar[EventType]
|
|
599
|
+
UPDATE_DIAGNOSIS_COMMAND__POST_UPDATE: _ClassVar[EventType]
|
|
600
|
+
UPDATE_DIAGNOSIS_COMMAND__PRE_COMMIT: _ClassVar[EventType]
|
|
601
|
+
UPDATE_DIAGNOSIS_COMMAND__POST_COMMIT: _ClassVar[EventType]
|
|
602
|
+
UPDATE_DIAGNOSIS_COMMAND__PRE_DELETE: _ClassVar[EventType]
|
|
603
|
+
UPDATE_DIAGNOSIS_COMMAND__POST_DELETE: _ClassVar[EventType]
|
|
604
|
+
UPDATE_DIAGNOSIS_COMMAND__PRE_ENTER_IN_ERROR: _ClassVar[EventType]
|
|
605
|
+
UPDATE_DIAGNOSIS_COMMAND__POST_ENTER_IN_ERROR: _ClassVar[EventType]
|
|
606
|
+
UPDATE_DIAGNOSIS_COMMAND__PRE_EXECUTE_ACTION: _ClassVar[EventType]
|
|
607
|
+
UPDATE_DIAGNOSIS_COMMAND__POST_EXECUTE_ACTION: _ClassVar[EventType]
|
|
608
|
+
UPDATE_DIAGNOSIS__CONDITION__PRE_SEARCH: _ClassVar[EventType]
|
|
609
|
+
UPDATE_DIAGNOSIS__CONDITION__POST_SEARCH: _ClassVar[EventType]
|
|
610
|
+
UPDATE_DIAGNOSIS__NEW_CONDITION__PRE_SEARCH: _ClassVar[EventType]
|
|
611
|
+
UPDATE_DIAGNOSIS__NEW_CONDITION__POST_SEARCH: _ClassVar[EventType]
|
|
568
612
|
UPDATE_GOAL_COMMAND__PRE_ORIGINATE: _ClassVar[EventType]
|
|
569
613
|
UPDATE_GOAL_COMMAND__POST_ORIGINATE: _ClassVar[EventType]
|
|
570
614
|
UPDATE_GOAL_COMMAND__PRE_UPDATE: _ClassVar[EventType]
|
|
@@ -810,6 +854,20 @@ ASSESS_COMMAND__PRE_EXECUTE_ACTION: EventType
|
|
|
810
854
|
ASSESS_COMMAND__POST_EXECUTE_ACTION: EventType
|
|
811
855
|
ASSESS__CONDITION__POST_SEARCH: EventType
|
|
812
856
|
ASSESS__CONDITION__PRE_SEARCH: EventType
|
|
857
|
+
CANCEL_PRESCRIPTION_COMMAND__PRE_ORIGINATE: EventType
|
|
858
|
+
CANCEL_PRESCRIPTION_COMMAND__POST_ORIGINATE: EventType
|
|
859
|
+
CANCEL_PRESCRIPTION_COMMAND__PRE_UPDATE: EventType
|
|
860
|
+
CANCEL_PRESCRIPTION_COMMAND__POST_UPDATE: EventType
|
|
861
|
+
CANCEL_PRESCRIPTION_COMMAND__PRE_COMMIT: EventType
|
|
862
|
+
CANCEL_PRESCRIPTION_COMMAND__POST_COMMIT: EventType
|
|
863
|
+
CANCEL_PRESCRIPTION_COMMAND__PRE_DELETE: EventType
|
|
864
|
+
CANCEL_PRESCRIPTION_COMMAND__POST_DELETE: EventType
|
|
865
|
+
CANCEL_PRESCRIPTION_COMMAND__PRE_ENTER_IN_ERROR: EventType
|
|
866
|
+
CANCEL_PRESCRIPTION_COMMAND__POST_ENTER_IN_ERROR: EventType
|
|
867
|
+
CANCEL_PRESCRIPTION_COMMAND__PRE_EXECUTE_ACTION: EventType
|
|
868
|
+
CANCEL_PRESCRIPTION_COMMAND__POST_EXECUTE_ACTION: EventType
|
|
869
|
+
CANCEL_PRESCRIPTION__SELECTED_PRESCRIPTION__PRE_SEARCH: EventType
|
|
870
|
+
CANCEL_PRESCRIPTION__SELECTED_PRESCRIPTION__POST_SEARCH: EventType
|
|
813
871
|
CLIPBOARD_COMMAND__PRE_ORIGINATE: EventType
|
|
814
872
|
CLIPBOARD_COMMAND__POST_ORIGINATE: EventType
|
|
815
873
|
CLIPBOARD_COMMAND__PRE_UPDATE: EventType
|
|
@@ -1177,6 +1235,20 @@ ROS_COMMAND__POST_EXECUTE_ACTION: EventType
|
|
|
1177
1235
|
ROS_COMMAND__POST_INSERTED_INTO_NOTE: EventType
|
|
1178
1236
|
ROS__QUESTIONNAIRE__PRE_SEARCH: EventType
|
|
1179
1237
|
ROS__QUESTIONNAIRE__POST_SEARCH: EventType
|
|
1238
|
+
SNOOZE_PROTOCOL_COMMAND__PRE_ORIGINATE: EventType
|
|
1239
|
+
SNOOZE_PROTOCOL_COMMAND__POST_ORIGINATE: EventType
|
|
1240
|
+
SNOOZE_PROTOCOL_COMMAND__PRE_UPDATE: EventType
|
|
1241
|
+
SNOOZE_PROTOCOL_COMMAND__POST_UPDATE: EventType
|
|
1242
|
+
SNOOZE_PROTOCOL_COMMAND__PRE_COMMIT: EventType
|
|
1243
|
+
SNOOZE_PROTOCOL_COMMAND__POST_COMMIT: EventType
|
|
1244
|
+
SNOOZE_PROTOCOL_COMMAND__PRE_DELETE: EventType
|
|
1245
|
+
SNOOZE_PROTOCOL_COMMAND__POST_DELETE: EventType
|
|
1246
|
+
SNOOZE_PROTOCOL_COMMAND__PRE_ENTER_IN_ERROR: EventType
|
|
1247
|
+
SNOOZE_PROTOCOL_COMMAND__POST_ENTER_IN_ERROR: EventType
|
|
1248
|
+
SNOOZE_PROTOCOL_COMMAND__PRE_EXECUTE_ACTION: EventType
|
|
1249
|
+
SNOOZE_PROTOCOL_COMMAND__POST_EXECUTE_ACTION: EventType
|
|
1250
|
+
SNOOZE_PROTOCOL__PROTOCOL__PRE_SEARCH: EventType
|
|
1251
|
+
SNOOZE_PROTOCOL__PROTOCOL__POST_SEARCH: EventType
|
|
1180
1252
|
STOP_MEDICATION_COMMAND__PRE_ORIGINATE: EventType
|
|
1181
1253
|
STOP_MEDICATION_COMMAND__POST_ORIGINATE: EventType
|
|
1182
1254
|
STOP_MEDICATION_COMMAND__PRE_UPDATE: EventType
|
|
@@ -1239,6 +1311,22 @@ TASK__ASSIGN_TO__PRE_SEARCH: EventType
|
|
|
1239
1311
|
TASK__ASSIGN_TO__POST_SEARCH: EventType
|
|
1240
1312
|
TASK__LABELS__PRE_SEARCH: EventType
|
|
1241
1313
|
TASK__LABELS__POST_SEARCH: EventType
|
|
1314
|
+
UPDATE_DIAGNOSIS_COMMAND__PRE_ORIGINATE: EventType
|
|
1315
|
+
UPDATE_DIAGNOSIS_COMMAND__POST_ORIGINATE: EventType
|
|
1316
|
+
UPDATE_DIAGNOSIS_COMMAND__PRE_UPDATE: EventType
|
|
1317
|
+
UPDATE_DIAGNOSIS_COMMAND__POST_UPDATE: EventType
|
|
1318
|
+
UPDATE_DIAGNOSIS_COMMAND__PRE_COMMIT: EventType
|
|
1319
|
+
UPDATE_DIAGNOSIS_COMMAND__POST_COMMIT: EventType
|
|
1320
|
+
UPDATE_DIAGNOSIS_COMMAND__PRE_DELETE: EventType
|
|
1321
|
+
UPDATE_DIAGNOSIS_COMMAND__POST_DELETE: EventType
|
|
1322
|
+
UPDATE_DIAGNOSIS_COMMAND__PRE_ENTER_IN_ERROR: EventType
|
|
1323
|
+
UPDATE_DIAGNOSIS_COMMAND__POST_ENTER_IN_ERROR: EventType
|
|
1324
|
+
UPDATE_DIAGNOSIS_COMMAND__PRE_EXECUTE_ACTION: EventType
|
|
1325
|
+
UPDATE_DIAGNOSIS_COMMAND__POST_EXECUTE_ACTION: EventType
|
|
1326
|
+
UPDATE_DIAGNOSIS__CONDITION__PRE_SEARCH: EventType
|
|
1327
|
+
UPDATE_DIAGNOSIS__CONDITION__POST_SEARCH: EventType
|
|
1328
|
+
UPDATE_DIAGNOSIS__NEW_CONDITION__PRE_SEARCH: EventType
|
|
1329
|
+
UPDATE_DIAGNOSIS__NEW_CONDITION__POST_SEARCH: EventType
|
|
1242
1330
|
UPDATE_GOAL_COMMAND__PRE_ORIGINATE: EventType
|
|
1243
1331
|
UPDATE_GOAL_COMMAND__POST_ORIGINATE: EventType
|
|
1244
1332
|
UPDATE_GOAL_COMMAND__PRE_UPDATE: EventType
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from collections.abc import Generator
|
|
2
2
|
from datetime import datetime
|
|
3
|
+
from typing import cast
|
|
3
4
|
|
|
4
5
|
import pytest
|
|
5
6
|
|
|
7
|
+
import settings
|
|
6
8
|
from canvas_sdk.commands.tests.test_utils import (
|
|
7
9
|
COMMANDS,
|
|
8
10
|
MaskedValue,
|
|
@@ -12,6 +14,7 @@ from canvas_sdk.commands.tests.test_utils import (
|
|
|
12
14
|
get_token,
|
|
13
15
|
install_plugin,
|
|
14
16
|
trigger_plugin_event,
|
|
17
|
+
wait_for_log,
|
|
15
18
|
write_protocol_code,
|
|
16
19
|
)
|
|
17
20
|
|
|
@@ -40,10 +43,18 @@ def write_and_install_protocol_and_clean_up(
|
|
|
40
43
|
) -> Generator[None, None, None]:
|
|
41
44
|
"""Write the protocol code, install the plugin, and clean up after the test."""
|
|
42
45
|
write_protocol_code(new_note["externallyExposableId"], plugin_name, COMMANDS)
|
|
46
|
+
message_received_event, thread, ws = wait_for_log(
|
|
47
|
+
cast(str, settings.INTEGRATION_TEST_URL),
|
|
48
|
+
token.value,
|
|
49
|
+
f"Loading plugin '{plugin_name}",
|
|
50
|
+
)
|
|
43
51
|
install_plugin(plugin_name, token)
|
|
52
|
+
message_received_event.wait(timeout=5.0)
|
|
44
53
|
|
|
45
54
|
yield
|
|
46
55
|
|
|
56
|
+
ws.close()
|
|
57
|
+
thread.join()
|
|
47
58
|
clean_up_files_and_plugins(plugin_name, token)
|
|
48
59
|
|
|
49
60
|
|
|
@@ -57,8 +68,7 @@ def test_protocol_that_inserts_every_command(
|
|
|
57
68
|
commands_in_body = get_original_note_body_commands(new_note["id"], token)
|
|
58
69
|
|
|
59
70
|
# TODO: Temporary workaround to ignore the updateGoal command until the integration test instance is fixed.
|
|
60
|
-
command_keys = [c.Meta.key for c in COMMANDS
|
|
61
|
-
|
|
71
|
+
command_keys = [c.Meta.key for c in COMMANDS]
|
|
62
72
|
assert len(command_keys) == len(commands_in_body)
|
|
63
73
|
for i, command_key in enumerate(command_keys):
|
|
64
74
|
assert commands_in_body[i] == command_key
|
|
@@ -190,11 +190,6 @@ class Protocol(BaseProtocol):
|
|
|
190
190
|
def install_plugin(plugin_name: str, token: MaskedValue) -> None:
|
|
191
191
|
"""Install a plugin."""
|
|
192
192
|
with open(_build_package(Path(f"./custom-plugins/{plugin_name}")), "rb") as package:
|
|
193
|
-
message_received_event = wait_for_log(
|
|
194
|
-
cast(str, settings.INTEGRATION_TEST_URL),
|
|
195
|
-
token.value,
|
|
196
|
-
f"Loading plugin '{plugin_name}",
|
|
197
|
-
)
|
|
198
193
|
response = requests.post(
|
|
199
194
|
plugin_url(cast(str, settings.INTEGRATION_TEST_URL)),
|
|
200
195
|
data={"is_enabled": True},
|
|
@@ -203,8 +198,6 @@ def install_plugin(plugin_name: str, token: MaskedValue) -> None:
|
|
|
203
198
|
)
|
|
204
199
|
response.raise_for_status()
|
|
205
200
|
|
|
206
|
-
message_received_event.wait(timeout=5.0)
|
|
207
|
-
|
|
208
201
|
|
|
209
202
|
def trigger_plugin_event(token: MaskedValue) -> None:
|
|
210
203
|
"""Trigger a plugin event."""
|
|
@@ -328,7 +321,9 @@ def get_token() -> MaskedValue:
|
|
|
328
321
|
return MaskedValue(response.json()["access_token"])
|
|
329
322
|
|
|
330
323
|
|
|
331
|
-
def wait_for_log(
|
|
324
|
+
def wait_for_log(
|
|
325
|
+
host: str, token: str, message: str
|
|
326
|
+
) -> tuple[threading.Event, threading.Thread, websocket.WebSocketApp]:
|
|
332
327
|
"""Wait for a specific log message."""
|
|
333
328
|
hostname = cast(str, urlparse(host).hostname)
|
|
334
329
|
instance = hostname.removesuffix(".canvasmedical.com")
|
|
@@ -362,4 +357,4 @@ def wait_for_log(host: str, token: str, message: str) -> threading.Event:
|
|
|
362
357
|
|
|
363
358
|
connected_event.wait(timeout=5.0)
|
|
364
359
|
|
|
365
|
-
return message_received_event
|
|
360
|
+
return message_received_event, thread, ws
|
|
@@ -92,10 +92,10 @@ class Protocol(BaseProtocol):
|
|
|
92
92
|
protocol.write(protocol_code)
|
|
93
93
|
|
|
94
94
|
with open(_build_package(Path(f"./custom-plugins/{plugin_name}")), "rb") as package:
|
|
95
|
-
message_received_event = wait_for_log(
|
|
95
|
+
message_received_event, thread, ws = wait_for_log(
|
|
96
96
|
settings.INTEGRATION_TEST_URL,
|
|
97
97
|
token.value,
|
|
98
|
-
f"Loading plugin '{plugin_name}
|
|
98
|
+
f"Loading plugin '{plugin_name}",
|
|
99
99
|
)
|
|
100
100
|
|
|
101
101
|
# install the plugin
|
|
@@ -111,6 +111,9 @@ class Protocol(BaseProtocol):
|
|
|
111
111
|
|
|
112
112
|
yield
|
|
113
113
|
|
|
114
|
+
ws.close()
|
|
115
|
+
thread.join()
|
|
116
|
+
|
|
114
117
|
# clean up
|
|
115
118
|
if Path(f"./custom-plugins/{plugin_name}").exists():
|
|
116
119
|
shutil.rmtree(Path(f"./custom-plugins/{plugin_name}"))
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .surescripts_messages import (
|
|
2
|
+
SendSurescriptsBenefitsRequestEffect,
|
|
3
|
+
SendSurescriptsEligibilityRequestEffect,
|
|
4
|
+
SendSurescriptsMedicationHistoryRequestEffect,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"SendSurescriptsBenefitsRequestEffect",
|
|
9
|
+
"SendSurescriptsEligibilityRequestEffect",
|
|
10
|
+
"SendSurescriptsMedicationHistoryRequestEffect",
|
|
11
|
+
]
|
canvas_sdk/effects/task/task.py
CHANGED
|
@@ -23,6 +23,7 @@ class AddTask(_BaseEffect):
|
|
|
23
23
|
apply_required_fields = ("title",)
|
|
24
24
|
|
|
25
25
|
assignee_id: str | None = None
|
|
26
|
+
team_id: str | None = None
|
|
26
27
|
patient_id: str | None = None
|
|
27
28
|
title: str | None = None
|
|
28
29
|
due: datetime | None = None
|
|
@@ -36,6 +37,7 @@ class AddTask(_BaseEffect):
|
|
|
36
37
|
"patient": {"id": self.patient_id},
|
|
37
38
|
"due": self.due.isoformat() if self.due else None,
|
|
38
39
|
"assignee": {"id": self.assignee_id},
|
|
40
|
+
"team": {"id": self.team_id},
|
|
39
41
|
"title": self.title,
|
|
40
42
|
"status": self.status.value,
|
|
41
43
|
"labels": self.labels,
|
|
@@ -74,6 +76,7 @@ class UpdateTask(_BaseEffect):
|
|
|
74
76
|
|
|
75
77
|
id: str | None = None
|
|
76
78
|
assignee_id: str | None = None
|
|
79
|
+
team_id: str | None = None
|
|
77
80
|
patient_id: str | None = None
|
|
78
81
|
title: str | None = None
|
|
79
82
|
due: datetime | None = None
|
canvas_sdk/utils/__init__.py
CHANGED
canvas_sdk/utils/http.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import concurrent
|
|
2
|
+
import functools
|
|
1
3
|
import time
|
|
2
|
-
from collections.abc import Callable, Mapping
|
|
4
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
3
6
|
from functools import wraps
|
|
4
|
-
from typing import Any, TypeVar, cast
|
|
7
|
+
from typing import Any, Literal, Protocol, TypeVar, cast
|
|
5
8
|
|
|
6
9
|
import requests
|
|
7
10
|
import statsd
|
|
@@ -9,9 +12,90 @@ import statsd
|
|
|
9
12
|
F = TypeVar("F", bound=Callable)
|
|
10
13
|
|
|
11
14
|
|
|
15
|
+
class _BatchableRequest:
|
|
16
|
+
"""Representation of a request that will be executed in parallel with other requests."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self, method: Literal["GET", "POST", "PUT", "PATCH"], url: str, **kwargs: Any
|
|
20
|
+
) -> None:
|
|
21
|
+
self._method = method
|
|
22
|
+
self._url = url
|
|
23
|
+
self._kwargs = kwargs
|
|
24
|
+
|
|
25
|
+
def fn(self, client: "Http") -> Callable:
|
|
26
|
+
"""
|
|
27
|
+
Return a callable constructed from an Http object and the method, URL, and kwargs.
|
|
28
|
+
|
|
29
|
+
The callable is passed to the ThreadPoolExecutor.
|
|
30
|
+
"""
|
|
31
|
+
client_method: Callable
|
|
32
|
+
match self._method:
|
|
33
|
+
case "GET":
|
|
34
|
+
client_method = client.get
|
|
35
|
+
case "POST":
|
|
36
|
+
client_method = client.post
|
|
37
|
+
case "PUT":
|
|
38
|
+
client_method = client.put
|
|
39
|
+
case "PATCH":
|
|
40
|
+
client_method = client.patch
|
|
41
|
+
case _:
|
|
42
|
+
raise ValueError(f"HTTP method {self._method} is not supported")
|
|
43
|
+
|
|
44
|
+
return functools.partial(client_method, self._url, **self._kwargs)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class BatchableRequest(Protocol):
|
|
48
|
+
"""Protocol for batchable requests."""
|
|
49
|
+
|
|
50
|
+
def fn(self, client: "Http") -> Callable:
|
|
51
|
+
"""
|
|
52
|
+
Return a callable that can be passed to the ThreadPoolExecutor.
|
|
53
|
+
"""
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def batch_get(
|
|
58
|
+
url: str, headers: Mapping[str, str | bytes | None] | None = None
|
|
59
|
+
) -> BatchableRequest:
|
|
60
|
+
"""Return a batchable GET request."""
|
|
61
|
+
return _BatchableRequest("GET", url, headers=headers)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def batch_post(
|
|
65
|
+
url: str,
|
|
66
|
+
json: dict | None = None,
|
|
67
|
+
data: dict | str | list | bytes | None = None,
|
|
68
|
+
headers: Mapping[str, str | bytes | None] | None = None,
|
|
69
|
+
) -> BatchableRequest:
|
|
70
|
+
"""Return a batchable POST request."""
|
|
71
|
+
return _BatchableRequest("POST", url, json=json, data=data, headeres=headers)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def batch_put(
|
|
75
|
+
url: str,
|
|
76
|
+
json: dict | None = None,
|
|
77
|
+
data: dict | str | list | bytes | None = None,
|
|
78
|
+
headers: Mapping[str, str | bytes | None] | None = None,
|
|
79
|
+
) -> BatchableRequest:
|
|
80
|
+
"""Return a batchable PUT request."""
|
|
81
|
+
return _BatchableRequest("PUT", url, json=json, data=data, headers=headers)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def batch_patch(
|
|
85
|
+
url: str,
|
|
86
|
+
json: dict | None = None,
|
|
87
|
+
data: dict | str | list | bytes | None = None,
|
|
88
|
+
headers: Mapping[str, str | bytes | None] | None = None,
|
|
89
|
+
) -> BatchableRequest:
|
|
90
|
+
"""Return a batchable PATCH request."""
|
|
91
|
+
return _BatchableRequest("PATCH", url, json=json, data=data, headers=headers)
|
|
92
|
+
|
|
93
|
+
|
|
12
94
|
class Http:
|
|
13
95
|
"""A helper class for completing HTTP calls with metrics tracking."""
|
|
14
96
|
|
|
97
|
+
_MAX_WORKER_TIMEOUT_SECONDS = 30
|
|
98
|
+
|
|
15
99
|
def __init__(self) -> None:
|
|
16
100
|
self.session = requests.Session()
|
|
17
101
|
self.statsd_client = statsd.StatsClient()
|
|
@@ -26,7 +110,7 @@ class Http:
|
|
|
26
110
|
result = fn(self, *args, **kwargs)
|
|
27
111
|
end_time = time.time()
|
|
28
112
|
timing = int((end_time - start_time) * 1000)
|
|
29
|
-
self.statsd_client.timing(f"http_{fn.__name__}", timing)
|
|
113
|
+
self.statsd_client.timing(f"plugins.http_{fn.__name__}", timing)
|
|
30
114
|
return result
|
|
31
115
|
|
|
32
116
|
return cast(F, wrapper)
|
|
@@ -72,3 +156,29 @@ class Http:
|
|
|
72
156
|
) -> requests.Response:
|
|
73
157
|
"""Sends a PATCH request."""
|
|
74
158
|
return self.session.patch(url, json=json, data=data, headers=headers)
|
|
159
|
+
|
|
160
|
+
@measure_time
|
|
161
|
+
def batch_requests(
|
|
162
|
+
self,
|
|
163
|
+
batch_requests: Iterable[BatchableRequest],
|
|
164
|
+
timeout: int | None = None,
|
|
165
|
+
) -> list[requests.Response]:
|
|
166
|
+
"""
|
|
167
|
+
Execute requests in parallel.
|
|
168
|
+
|
|
169
|
+
Wait for the responses to complete, and then return a list of the responses in the same
|
|
170
|
+
ordering as the requests.
|
|
171
|
+
"""
|
|
172
|
+
if timeout is None:
|
|
173
|
+
timeout = self._MAX_WORKER_TIMEOUT_SECONDS
|
|
174
|
+
elif timeout < 1 or timeout > self._MAX_WORKER_TIMEOUT_SECONDS:
|
|
175
|
+
raise ValueError(
|
|
176
|
+
f"Timeout value must be greater than 0 and less than or equal to {self._MAX_WORKER_TIMEOUT_SECONDS} seconds"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
with ThreadPoolExecutor() as executor:
|
|
180
|
+
futures = [executor.submit(request.fn(self)) for request in batch_requests]
|
|
181
|
+
|
|
182
|
+
concurrent.futures.wait(futures, timeout=timeout)
|
|
183
|
+
|
|
184
|
+
return [future.result() for future in futures]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from django.contrib.postgres.fields import ArrayField
|
|
2
|
+
from django.db import models
|
|
3
|
+
|
|
4
|
+
from canvas_sdk.v1.data.common import ContactPointState, ContactPointSystem, ContactPointUse
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TeamResponsibility(models.TextChoices):
|
|
8
|
+
"""TeamResponsibility."""
|
|
9
|
+
|
|
10
|
+
COLLECT_SPECIMENS_FROM_PATIENT = (
|
|
11
|
+
"COLLECT_SPECIMENS_FROM_PATIENT",
|
|
12
|
+
"Collect specimens from a patient",
|
|
13
|
+
)
|
|
14
|
+
COMMUNICATE_DIAGNOSTIC_RESULTS_TO_PATIENT = (
|
|
15
|
+
"COMMUNICATE_DIAGNOSTIC_RESULTS_TO_PATIENT",
|
|
16
|
+
"Communicate diagnostic results to patient",
|
|
17
|
+
)
|
|
18
|
+
COORDINATE_REFERRALS_FOR_PATIENT = (
|
|
19
|
+
"COORDINATE_REFERRALS_FOR_PATIENT",
|
|
20
|
+
"Coordinate referrals for a patient",
|
|
21
|
+
)
|
|
22
|
+
PROCESS_REFILL_REQUESTS = "PROCESS_REFILL_REQUESTS", "Process refill requests from a pharmacy"
|
|
23
|
+
PROCESS_CHANGE_REQUESTS = "PROCESS_CHANGE_REQUESTS", "Process change requests from a pharmacy"
|
|
24
|
+
SCHEDULE_LAB_VISITS_FOR_PATIENT = (
|
|
25
|
+
"SCHEDULE_LAB_VISITS_FOR_PATIENT",
|
|
26
|
+
"Schedule lab visits for a patient",
|
|
27
|
+
)
|
|
28
|
+
POPULATION_HEALTH_CAMPAIGN_OUTREACH = (
|
|
29
|
+
"POPULATION_HEALTH_CAMPAIGN_OUTREACH",
|
|
30
|
+
"Population health campaign outreach",
|
|
31
|
+
)
|
|
32
|
+
COLLECT_PATIENT_PAYMENTS = "COLLECT_PATIENT_PAYMENTS", "Collect patient payments"
|
|
33
|
+
COMPLETE_OPEN_LAB_ORDERS = "COMPLETE_OPEN_LAB_ORDERS", "Complete open lab orders"
|
|
34
|
+
REVIEW_ERA_POSTING_EXCEPTIONS = (
|
|
35
|
+
"REVIEW_ERA_POSTING_EXCEPTIONS",
|
|
36
|
+
"Review electronic remittance posting exceptions",
|
|
37
|
+
)
|
|
38
|
+
REVIEW_COVERAGES = "REVIEW_COVERAGES", "Review incomplete patient coverages"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Team(models.Model):
|
|
42
|
+
"""Team."""
|
|
43
|
+
|
|
44
|
+
class Meta:
|
|
45
|
+
managed = False
|
|
46
|
+
db_table = "canvas_sdk_data_api_team_001"
|
|
47
|
+
|
|
48
|
+
id = models.UUIDField()
|
|
49
|
+
dbid = models.BigIntegerField(primary_key=True)
|
|
50
|
+
created = models.DateTimeField()
|
|
51
|
+
modified = models.DateTimeField()
|
|
52
|
+
name = models.CharField()
|
|
53
|
+
responsibilities = ArrayField(models.CharField(choices=TeamResponsibility.choices))
|
|
54
|
+
members = models.ManyToManyField( # type: ignore[var-annotated]
|
|
55
|
+
"v1.Staff", # type: ignore[misc]
|
|
56
|
+
related_name="teams",
|
|
57
|
+
db_table="canvas_sdk_data_api_team_members_001",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TeamContactPoint(models.Model):
|
|
62
|
+
"""TeamContactPoint."""
|
|
63
|
+
|
|
64
|
+
class Meta:
|
|
65
|
+
managed = False
|
|
66
|
+
db_table = "canvas_sdk_data_api_teamcontactpoint_001"
|
|
67
|
+
|
|
68
|
+
id = models.UUIDField()
|
|
69
|
+
dbid = models.BigIntegerField(primary_key=True)
|
|
70
|
+
system = models.CharField(choices=ContactPointSystem.choices)
|
|
71
|
+
value = models.CharField()
|
|
72
|
+
use = models.CharField(choices=ContactPointUse.choices)
|
|
73
|
+
use_notes = models.CharField()
|
|
74
|
+
rank = models.IntegerField()
|
|
75
|
+
state = models.CharField(choices=ContactPointState.choices)
|
|
76
|
+
team = models.ForeignKey(Team, on_delete=models.DO_NOTHING, related_name="telecom")
|
|
@@ -14,9 +14,17 @@ import requests
|
|
|
14
14
|
from psycopg import Connection
|
|
15
15
|
from psycopg.rows import dict_row
|
|
16
16
|
|
|
17
|
-
import settings
|
|
18
17
|
from plugin_runner.aws_headers import aws_sig_v4_headers
|
|
19
18
|
from plugin_runner.exceptions import InvalidPluginFormat, PluginInstallationError
|
|
19
|
+
from settings import (
|
|
20
|
+
AWS_ACCESS_KEY_ID,
|
|
21
|
+
AWS_REGION,
|
|
22
|
+
AWS_SECRET_ACCESS_KEY,
|
|
23
|
+
CUSTOMER_IDENTIFIER,
|
|
24
|
+
MEDIA_S3_BUCKET_NAME,
|
|
25
|
+
PLUGIN_DIRECTORY,
|
|
26
|
+
SECRETS_FILE_NAME,
|
|
27
|
+
)
|
|
20
28
|
|
|
21
29
|
# Plugin "packages" include this prefix in the database record for the plugin and the S3 bucket key.
|
|
22
30
|
UPLOAD_TO_PREFIX = "plugins"
|
|
@@ -100,19 +108,19 @@ def _extract_rows_to_dict(rows: list) -> dict[str, PluginAttributes]:
|
|
|
100
108
|
def download_plugin(plugin_package: str) -> Generator[Path, None, None]:
|
|
101
109
|
"""Download the plugin package from the S3 bucket."""
|
|
102
110
|
method = "GET"
|
|
103
|
-
host = f"s3-{
|
|
104
|
-
bucket =
|
|
105
|
-
customer_identifier =
|
|
111
|
+
host = f"s3-{AWS_REGION}.amazonaws.com"
|
|
112
|
+
bucket = MEDIA_S3_BUCKET_NAME
|
|
113
|
+
customer_identifier = CUSTOMER_IDENTIFIER
|
|
106
114
|
path = f"/{bucket}/{customer_identifier}/{plugin_package}"
|
|
107
115
|
payload = b"This is required for the AWS headers because it is part of the signature"
|
|
108
116
|
pre_auth_headers: dict[str, str] = {}
|
|
109
117
|
query: dict[str, str] = {}
|
|
110
118
|
headers = aws_sig_v4_headers(
|
|
111
|
-
|
|
112
|
-
|
|
119
|
+
AWS_ACCESS_KEY_ID,
|
|
120
|
+
AWS_SECRET_ACCESS_KEY,
|
|
113
121
|
pre_auth_headers,
|
|
114
122
|
"s3",
|
|
115
|
-
|
|
123
|
+
AWS_REGION,
|
|
116
124
|
host,
|
|
117
125
|
method,
|
|
118
126
|
path,
|
|
@@ -135,7 +143,7 @@ def install_plugin(plugin_name: str, attributes: PluginAttributes) -> None:
|
|
|
135
143
|
try:
|
|
136
144
|
print(f"Installing plugin '{plugin_name}'")
|
|
137
145
|
|
|
138
|
-
plugin_installation_path = Path(
|
|
146
|
+
plugin_installation_path = Path(PLUGIN_DIRECTORY) / plugin_name
|
|
139
147
|
|
|
140
148
|
# if plugin exists, first uninstall it
|
|
141
149
|
if plugin_installation_path.exists():
|
|
@@ -175,7 +183,7 @@ def install_plugin_secrets(plugin_name: str, secrets: dict[str, str]) -> None:
|
|
|
175
183
|
"""Write the plugin's secrets to disk in the package's directory."""
|
|
176
184
|
print(f"Writing plugin secrets for '{plugin_name}'")
|
|
177
185
|
|
|
178
|
-
secrets_path = Path(
|
|
186
|
+
secrets_path = Path(PLUGIN_DIRECTORY) / plugin_name / SECRETS_FILE_NAME
|
|
179
187
|
|
|
180
188
|
# Did the plugin ship a secrets.json? TOO BAD, IT'S GONE NOW.
|
|
181
189
|
if Path(secrets_path).exists():
|
|
@@ -199,7 +207,7 @@ def disable_plugin(plugin_name: str) -> None:
|
|
|
199
207
|
|
|
200
208
|
def uninstall_plugin(plugin_name: str) -> None:
|
|
201
209
|
"""Remove the plugin from the filesystem."""
|
|
202
|
-
plugin_path = Path(
|
|
210
|
+
plugin_path = Path(PLUGIN_DIRECTORY) / plugin_name
|
|
203
211
|
|
|
204
212
|
if plugin_path.exists():
|
|
205
213
|
shutil.rmtree(plugin_path)
|
|
@@ -207,10 +215,10 @@ def uninstall_plugin(plugin_name: str) -> None:
|
|
|
207
215
|
|
|
208
216
|
def install_plugins() -> None:
|
|
209
217
|
"""Install all enabled plugins."""
|
|
210
|
-
if Path(
|
|
211
|
-
shutil.rmtree(
|
|
218
|
+
if Path(PLUGIN_DIRECTORY).exists():
|
|
219
|
+
shutil.rmtree(PLUGIN_DIRECTORY)
|
|
212
220
|
|
|
213
|
-
os.mkdir(
|
|
221
|
+
os.mkdir(PLUGIN_DIRECTORY)
|
|
214
222
|
|
|
215
223
|
for plugin_name, attributes in enabled_plugins().items():
|
|
216
224
|
try:
|