canvas 0.35.1__py3-none-any.whl → 0.37.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.35.1.dist-info → canvas-0.37.0.dist-info}/METADATA +1 -1
- {canvas-0.35.1.dist-info → canvas-0.37.0.dist-info}/RECORD +42 -34
- canvas_cli/apps/plugin/plugin.py +3 -2
- canvas_cli/utils/validators/manifest_schema.py +15 -1
- canvas_generated/messages/effects_pb2.py +2 -2
- canvas_generated/messages/effects_pb2.pyi +8 -0
- canvas_generated/messages/events_pb2.py +2 -2
- canvas_generated/messages/events_pb2.pyi +26 -0
- canvas_sdk/caching/__init__.py +1 -0
- canvas_sdk/caching/base.py +127 -0
- canvas_sdk/caching/client.py +24 -0
- canvas_sdk/caching/exceptions.py +21 -0
- canvas_sdk/caching/plugins.py +20 -0
- canvas_sdk/caching/utils.py +28 -0
- canvas_sdk/commands/__init__.py +2 -0
- canvas_sdk/commands/commands/chart_section_review.py +23 -0
- canvas_sdk/commands/commands/prescribe.py +3 -0
- canvas_sdk/effects/launch_modal.py +1 -0
- canvas_sdk/effects/simple_api.py +61 -2
- canvas_sdk/handlers/simple_api/websocket.py +79 -0
- canvas_sdk/value_set/custom.py +1 -1
- canvas_sdk/value_set/v2022/allergy.py +1 -1
- canvas_sdk/value_set/v2022/assessment.py +1 -1
- canvas_sdk/value_set/v2022/communication.py +1 -1
- canvas_sdk/value_set/v2022/condition.py +1 -1
- canvas_sdk/value_set/v2022/device.py +1 -1
- canvas_sdk/value_set/v2022/diagnostic_study.py +1 -1
- canvas_sdk/value_set/v2022/encounter.py +1 -1
- canvas_sdk/value_set/v2022/immunization.py +1 -1
- canvas_sdk/value_set/v2022/individual_characteristic.py +1 -1
- canvas_sdk/value_set/v2022/intervention.py +1 -1
- canvas_sdk/value_set/v2022/laboratory_test.py +1 -1
- canvas_sdk/value_set/v2022/medication.py +1 -1
- canvas_sdk/value_set/v2022/physical_exam.py +1 -1
- canvas_sdk/value_set/v2022/procedure.py +1 -1
- plugin_runner/plugin_runner.py +15 -1
- plugin_runner/sandbox.py +5 -0
- protobufs/canvas_generated/messages/effects.proto +6 -0
- protobufs/canvas_generated/messages/events.proto +14 -12
- settings.py +28 -0
- {canvas-0.35.1.dist-info → canvas-0.37.0.dist-info}/WHEEL +0 -0
- {canvas-0.35.1.dist-info → canvas-0.37.0.dist-info}/entry_points.txt +0 -0
|
@@ -129,6 +129,8 @@ class EventType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
|
|
129
129
|
ADJUST_PRESCRIPTION__PHARMACY__POST_SEARCH: _ClassVar[EventType]
|
|
130
130
|
ADJUST_PRESCRIPTION__CHANGE_MEDICATION_TO__PRE_SEARCH: _ClassVar[EventType]
|
|
131
131
|
ADJUST_PRESCRIPTION__CHANGE_MEDICATION_TO__POST_SEARCH: _ClassVar[EventType]
|
|
132
|
+
ADJUST_PRESCRIPTION__SUPERVISING_PROVIDER__PRE_SEARCH: _ClassVar[EventType]
|
|
133
|
+
ADJUST_PRESCRIPTION__SUPERVISING_PROVIDER__POST_SEARCH: _ClassVar[EventType]
|
|
132
134
|
ALLERGY_COMMAND__PRE_ORIGINATE: _ClassVar[EventType]
|
|
133
135
|
ALLERGY_COMMAND__POST_ORIGINATE: _ClassVar[EventType]
|
|
134
136
|
ALLERGY_COMMAND__PRE_UPDATE: _ClassVar[EventType]
|
|
@@ -173,6 +175,12 @@ class EventType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
|
|
173
175
|
CANCEL_PRESCRIPTION_COMMAND__POST_EXECUTE_ACTION: _ClassVar[EventType]
|
|
174
176
|
CANCEL_PRESCRIPTION__SELECTED_PRESCRIPTION__PRE_SEARCH: _ClassVar[EventType]
|
|
175
177
|
CANCEL_PRESCRIPTION__SELECTED_PRESCRIPTION__POST_SEARCH: _ClassVar[EventType]
|
|
178
|
+
CHART_SECTION_REVIEW_COMMAND__PRE_ORIGINATE: _ClassVar[EventType]
|
|
179
|
+
CHART_SECTION_REVIEW_COMMAND__POST_ORIGINATE: _ClassVar[EventType]
|
|
180
|
+
CHART_SECTION_REVIEW_COMMAND__PRE_ENTER_IN_ERROR: _ClassVar[EventType]
|
|
181
|
+
CHART_SECTION_REVIEW_COMMAND__POST_ENTER_IN_ERROR: _ClassVar[EventType]
|
|
182
|
+
CHART_SECTION_REVIEW_COMMAND__PRE_EXECUTE_ACTION: _ClassVar[EventType]
|
|
183
|
+
CHART_SECTION_REVIEW_COMMAND__POST_EXECUTE_ACTION: _ClassVar[EventType]
|
|
176
184
|
CLIPBOARD_COMMAND__PRE_ORIGINATE: _ClassVar[EventType]
|
|
177
185
|
CLIPBOARD_COMMAND__POST_ORIGINATE: _ClassVar[EventType]
|
|
178
186
|
CLIPBOARD_COMMAND__PRE_UPDATE: _ClassVar[EventType]
|
|
@@ -478,6 +486,8 @@ class EventType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
|
|
478
486
|
PRESCRIBE__INDICATIONS__POST_SEARCH: _ClassVar[EventType]
|
|
479
487
|
PRESCRIBE__PHARMACY__PRE_SEARCH: _ClassVar[EventType]
|
|
480
488
|
PRESCRIBE__PHARMACY__POST_SEARCH: _ClassVar[EventType]
|
|
489
|
+
PRESCRIBE__SUPERVISING_PROVIDER__POST_SEARCH: _ClassVar[EventType]
|
|
490
|
+
PRESCRIBE__SUPERVISING_PROVIDER__PRE_SEARCH: _ClassVar[EventType]
|
|
481
491
|
QUESTIONNAIRE_COMMAND__PRE_ORIGINATE: _ClassVar[EventType]
|
|
482
492
|
QUESTIONNAIRE_COMMAND__POST_ORIGINATE: _ClassVar[EventType]
|
|
483
493
|
QUESTIONNAIRE_COMMAND__PRE_UPDATE: _ClassVar[EventType]
|
|
@@ -548,6 +558,8 @@ class EventType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
|
|
548
558
|
REFILL__INDICATIONS__POST_SEARCH: _ClassVar[EventType]
|
|
549
559
|
REFILL__PHARMACY__PRE_SEARCH: _ClassVar[EventType]
|
|
550
560
|
REFILL__PHARMACY__POST_SEARCH: _ClassVar[EventType]
|
|
561
|
+
REFILL__SUPERVISING_PROVIDER__PRE_SEARCH: _ClassVar[EventType]
|
|
562
|
+
REFILL__SUPERVISING_PROVIDER__POST_SEARCH: _ClassVar[EventType]
|
|
551
563
|
REMOVE_ALLERGY_COMMAND__PRE_ORIGINATE: _ClassVar[EventType]
|
|
552
564
|
REMOVE_ALLERGY_COMMAND__POST_ORIGINATE: _ClassVar[EventType]
|
|
553
565
|
REMOVE_ALLERGY_COMMAND__PRE_UPDATE: _ClassVar[EventType]
|
|
@@ -825,6 +837,7 @@ class EventType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
|
|
825
837
|
SHOW_CHART_SUMMARY_CODING_GAPS_SECTION_BUTTON: _ClassVar[EventType]
|
|
826
838
|
SIMPLE_API_AUTHENTICATE: _ClassVar[EventType]
|
|
827
839
|
SIMPLE_API_REQUEST: _ClassVar[EventType]
|
|
840
|
+
SIMPLE_API_WEBSOCKET_AUTHENTICATE: _ClassVar[EventType]
|
|
828
841
|
UNKNOWN: EventType
|
|
829
842
|
ALLERGY_INTOLERANCE_CREATED: EventType
|
|
830
843
|
ALLERGY_INTOLERANCE_UPDATED: EventType
|
|
@@ -945,6 +958,8 @@ ADJUST_PRESCRIPTION__PHARMACY__PRE_SEARCH: EventType
|
|
|
945
958
|
ADJUST_PRESCRIPTION__PHARMACY__POST_SEARCH: EventType
|
|
946
959
|
ADJUST_PRESCRIPTION__CHANGE_MEDICATION_TO__PRE_SEARCH: EventType
|
|
947
960
|
ADJUST_PRESCRIPTION__CHANGE_MEDICATION_TO__POST_SEARCH: EventType
|
|
961
|
+
ADJUST_PRESCRIPTION__SUPERVISING_PROVIDER__PRE_SEARCH: EventType
|
|
962
|
+
ADJUST_PRESCRIPTION__SUPERVISING_PROVIDER__POST_SEARCH: EventType
|
|
948
963
|
ALLERGY_COMMAND__PRE_ORIGINATE: EventType
|
|
949
964
|
ALLERGY_COMMAND__POST_ORIGINATE: EventType
|
|
950
965
|
ALLERGY_COMMAND__PRE_UPDATE: EventType
|
|
@@ -989,6 +1004,12 @@ CANCEL_PRESCRIPTION_COMMAND__PRE_EXECUTE_ACTION: EventType
|
|
|
989
1004
|
CANCEL_PRESCRIPTION_COMMAND__POST_EXECUTE_ACTION: EventType
|
|
990
1005
|
CANCEL_PRESCRIPTION__SELECTED_PRESCRIPTION__PRE_SEARCH: EventType
|
|
991
1006
|
CANCEL_PRESCRIPTION__SELECTED_PRESCRIPTION__POST_SEARCH: EventType
|
|
1007
|
+
CHART_SECTION_REVIEW_COMMAND__PRE_ORIGINATE: EventType
|
|
1008
|
+
CHART_SECTION_REVIEW_COMMAND__POST_ORIGINATE: EventType
|
|
1009
|
+
CHART_SECTION_REVIEW_COMMAND__PRE_ENTER_IN_ERROR: EventType
|
|
1010
|
+
CHART_SECTION_REVIEW_COMMAND__POST_ENTER_IN_ERROR: EventType
|
|
1011
|
+
CHART_SECTION_REVIEW_COMMAND__PRE_EXECUTE_ACTION: EventType
|
|
1012
|
+
CHART_SECTION_REVIEW_COMMAND__POST_EXECUTE_ACTION: EventType
|
|
992
1013
|
CLIPBOARD_COMMAND__PRE_ORIGINATE: EventType
|
|
993
1014
|
CLIPBOARD_COMMAND__POST_ORIGINATE: EventType
|
|
994
1015
|
CLIPBOARD_COMMAND__PRE_UPDATE: EventType
|
|
@@ -1294,6 +1315,8 @@ PRESCRIBE__INDICATIONS__PRE_SEARCH: EventType
|
|
|
1294
1315
|
PRESCRIBE__INDICATIONS__POST_SEARCH: EventType
|
|
1295
1316
|
PRESCRIBE__PHARMACY__PRE_SEARCH: EventType
|
|
1296
1317
|
PRESCRIBE__PHARMACY__POST_SEARCH: EventType
|
|
1318
|
+
PRESCRIBE__SUPERVISING_PROVIDER__POST_SEARCH: EventType
|
|
1319
|
+
PRESCRIBE__SUPERVISING_PROVIDER__PRE_SEARCH: EventType
|
|
1297
1320
|
QUESTIONNAIRE_COMMAND__PRE_ORIGINATE: EventType
|
|
1298
1321
|
QUESTIONNAIRE_COMMAND__POST_ORIGINATE: EventType
|
|
1299
1322
|
QUESTIONNAIRE_COMMAND__PRE_UPDATE: EventType
|
|
@@ -1364,6 +1387,8 @@ REFILL__INDICATIONS__PRE_SEARCH: EventType
|
|
|
1364
1387
|
REFILL__INDICATIONS__POST_SEARCH: EventType
|
|
1365
1388
|
REFILL__PHARMACY__PRE_SEARCH: EventType
|
|
1366
1389
|
REFILL__PHARMACY__POST_SEARCH: EventType
|
|
1390
|
+
REFILL__SUPERVISING_PROVIDER__PRE_SEARCH: EventType
|
|
1391
|
+
REFILL__SUPERVISING_PROVIDER__POST_SEARCH: EventType
|
|
1367
1392
|
REMOVE_ALLERGY_COMMAND__PRE_ORIGINATE: EventType
|
|
1368
1393
|
REMOVE_ALLERGY_COMMAND__POST_ORIGINATE: EventType
|
|
1369
1394
|
REMOVE_ALLERGY_COMMAND__PRE_UPDATE: EventType
|
|
@@ -1641,6 +1666,7 @@ SHOW_CHART_SUMMARY_FAMILY_HISTORY_SECTION_BUTTON: EventType
|
|
|
1641
1666
|
SHOW_CHART_SUMMARY_CODING_GAPS_SECTION_BUTTON: EventType
|
|
1642
1667
|
SIMPLE_API_AUTHENTICATE: EventType
|
|
1643
1668
|
SIMPLE_API_REQUEST: EventType
|
|
1669
|
+
SIMPLE_API_WEBSOCKET_AUTHENTICATE: EventType
|
|
1644
1670
|
|
|
1645
1671
|
class Event(_message.Message):
|
|
1646
1672
|
__slots__ = ("type", "target", "context", "target_type")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__al__ = __exports__ = ()
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from django.core.cache import BaseCache
|
|
5
|
+
|
|
6
|
+
from canvas_sdk.caching.exceptions import CachingException
|
|
7
|
+
from canvas_sdk.caching.utils import WriteOnceProperty
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Cache:
|
|
11
|
+
"""A Class wrapper for interacting with cache."""
|
|
12
|
+
|
|
13
|
+
_connection = WriteOnceProperty[BaseCache]()
|
|
14
|
+
_prefix = WriteOnceProperty[str]()
|
|
15
|
+
_max_timeout_seconds = WriteOnceProperty[int | None]()
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self, connection: BaseCache, prefix: str = "", max_timeout_seconds: int | None = None
|
|
19
|
+
) -> None:
|
|
20
|
+
self._connection = connection
|
|
21
|
+
self._prefix = prefix
|
|
22
|
+
self._max_timeout_seconds = max_timeout_seconds
|
|
23
|
+
|
|
24
|
+
def _make_key(self, key: str) -> str:
|
|
25
|
+
return f"{self._prefix}:{key}" if self._prefix else key
|
|
26
|
+
|
|
27
|
+
def _get_timeout(self, timeout_seconds: int | None) -> int | None:
|
|
28
|
+
if timeout_seconds is None:
|
|
29
|
+
return self._max_timeout_seconds
|
|
30
|
+
|
|
31
|
+
if self._max_timeout_seconds is not None and timeout_seconds > self._max_timeout_seconds:
|
|
32
|
+
raise CachingException(
|
|
33
|
+
f"Timeout of {timeout_seconds} seconds exceeds the max timeout of {self._max_timeout_seconds} seconds."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return timeout_seconds
|
|
37
|
+
|
|
38
|
+
def set(self, key: str, value: Any, timeout_seconds: int | None = None) -> None:
|
|
39
|
+
"""Set a value in the cache.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
key: The cache key.
|
|
43
|
+
value: The value to cache.
|
|
44
|
+
timeout_seconds: The number of seconds for which the value should be cached.
|
|
45
|
+
"""
|
|
46
|
+
key = self._make_key(key)
|
|
47
|
+
self._connection.set(key, value, self._get_timeout(timeout_seconds))
|
|
48
|
+
|
|
49
|
+
def set_many(self, data: dict[str, Any], timeout_seconds: int | None = None) -> list[str]:
|
|
50
|
+
"""Set multiple values in the cache simultaneously.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
data: A dict of cache keys and their values.
|
|
54
|
+
timeout_seconds: The number of seconds for which to cache the data.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Returns a list of cache keys that failed insertion if supported by
|
|
58
|
+
the backend. Otherwise, an empty list is returned.
|
|
59
|
+
"""
|
|
60
|
+
data = {self._make_key(key): value for key, value in data.items()}
|
|
61
|
+
return self._connection.set_many(data, self._get_timeout(timeout_seconds))
|
|
62
|
+
|
|
63
|
+
def get(self, key: str, default: Any | None = None) -> Any:
|
|
64
|
+
"""Fetch a given key from the cache.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
key: The cache key.
|
|
68
|
+
default: The value to return if the key does not exist.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
The cached value, or the default if the key does not exist.
|
|
72
|
+
"""
|
|
73
|
+
key = self._make_key(key)
|
|
74
|
+
return self._connection.get(key, default)
|
|
75
|
+
|
|
76
|
+
def get_or_set(
|
|
77
|
+
self, key: str, default: Any | None = None, timeout_seconds: int | None = None
|
|
78
|
+
) -> Any:
|
|
79
|
+
"""Fetch a given key from the cache, or set it to the given default.
|
|
80
|
+
|
|
81
|
+
If the key does not exist, it will be created with the given default as
|
|
82
|
+
its value. If the default is a callable, it will be called with no
|
|
83
|
+
arguments and the return value will be used.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
key: The key to retrieve.
|
|
87
|
+
default: The default value to set if the key does not exist. May be
|
|
88
|
+
a callable with no arguments.
|
|
89
|
+
timeout_seconds: The number of seconds for which to cache the key.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
The cached value.
|
|
93
|
+
"""
|
|
94
|
+
key = self._make_key(key)
|
|
95
|
+
return self._connection.get_or_set(key, default, self._get_timeout(timeout_seconds))
|
|
96
|
+
|
|
97
|
+
def get_many(self, keys: Iterable[str]) -> Any:
|
|
98
|
+
"""Fetch multiple values from the cache.
|
|
99
|
+
|
|
100
|
+
This is often much faster than retrieving cached values individually,
|
|
101
|
+
and its use is encouraged where possible.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
keys: The cache keys to retrieve.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
A dict mapping each key in 'keys' to its cached value.
|
|
108
|
+
"""
|
|
109
|
+
keys = {self._make_key(key) for key in keys}
|
|
110
|
+
return self._connection.get_many(keys)
|
|
111
|
+
|
|
112
|
+
def delete(self, key: str) -> None:
|
|
113
|
+
"""Delete a key from the cache.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
key: The key to be removed from the cache.
|
|
117
|
+
"""
|
|
118
|
+
key = self._make_key(key)
|
|
119
|
+
self._connection.delete(key)
|
|
120
|
+
|
|
121
|
+
def __contains__(self, key: str) -> bool:
|
|
122
|
+
"""Return True if the key is in the cache and has not expired."""
|
|
123
|
+
key = self._make_key(key)
|
|
124
|
+
return self._connection.__contains__(key)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
__exports__ = ()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from django.core.cache import InvalidCacheBackendError
|
|
2
|
+
from django.core.cache import caches as django_caches
|
|
3
|
+
|
|
4
|
+
from canvas_sdk.caching.base import Cache
|
|
5
|
+
from canvas_sdk.caching.exceptions import CacheConfigurationError
|
|
6
|
+
|
|
7
|
+
caches: dict[tuple[str, str], Cache] = {}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_cache(
|
|
11
|
+
driver: str = "default", prefix: str = "", max_timeout_seconds: int | None = None
|
|
12
|
+
) -> Cache:
|
|
13
|
+
"""Get the cache client based on the specified driver."""
|
|
14
|
+
try:
|
|
15
|
+
key = (driver, prefix)
|
|
16
|
+
connection = django_caches[driver]
|
|
17
|
+
if key not in caches:
|
|
18
|
+
caches[key] = Cache(connection, prefix, max_timeout_seconds)
|
|
19
|
+
return caches[key]
|
|
20
|
+
except InvalidCacheBackendError as error:
|
|
21
|
+
raise CacheConfigurationError(driver) from error
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__exports__ = ()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CachingException(Exception):
|
|
5
|
+
"""A Generic Exception for the module."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CacheConfigurationError(CachingException):
|
|
9
|
+
"""An Exception raised when cache driver doesn't exist."""
|
|
10
|
+
|
|
11
|
+
driver: str
|
|
12
|
+
|
|
13
|
+
def __init__(self, driver: str, *args: Any):
|
|
14
|
+
super().__init__(args)
|
|
15
|
+
self.driver = driver
|
|
16
|
+
|
|
17
|
+
def __str__(self) -> str:
|
|
18
|
+
return f"The cache driver {self.driver} does not exist"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
__exports__ = ("CachingException", "CacheConfigurationError")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from canvas_sdk.caching.client import get_cache as get_cache_client
|
|
6
|
+
from canvas_sdk.utils.plugins import plugin_only
|
|
7
|
+
from settings import CANVAS_SDK_CACHE_TIMEOUT_SECONDS
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from canvas_sdk.caching.base import Cache
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@plugin_only
|
|
14
|
+
def get_cache(**kwargs: Any) -> Cache:
|
|
15
|
+
"""Get the cache client for plugins."""
|
|
16
|
+
prefix = kwargs["plugin_name"]
|
|
17
|
+
return get_cache_client("plugins", prefix, CANVAS_SDK_CACHE_TIMEOUT_SECONDS)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__exports__ = ("get_cache",)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from typing import Any, Generic, TypeVar
|
|
2
|
+
|
|
3
|
+
T = TypeVar("T")
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class WriteOnceProperty(Generic[T]):
|
|
7
|
+
"""A descriptor for write-once values."""
|
|
8
|
+
|
|
9
|
+
def __set_name__(self, owner: Any, name: str) -> None:
|
|
10
|
+
"""Set the name of the property so that the descriptor can access it."""
|
|
11
|
+
self.public_name = name
|
|
12
|
+
self.private_name = f"_{name}"
|
|
13
|
+
|
|
14
|
+
def __get__(self, obj: Any, objtype: Any = None) -> T:
|
|
15
|
+
"""Retrieve the value of the property."""
|
|
16
|
+
return getattr(obj, self.private_name)
|
|
17
|
+
|
|
18
|
+
def __set__(self, obj: Any, value: T) -> None:
|
|
19
|
+
"""Set the value of the property.
|
|
20
|
+
|
|
21
|
+
Throws an AttributeError if the property already has a value.
|
|
22
|
+
"""
|
|
23
|
+
if hasattr(obj, self.private_name):
|
|
24
|
+
raise AttributeError(f"{self.public_name} cannot be changed.")
|
|
25
|
+
setattr(obj, self.private_name, value)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
__exports__ = ()
|
canvas_sdk/commands/__init__.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from canvas_sdk.commands.commands.adjust_prescription import AdjustPrescriptionCommand
|
|
2
2
|
from canvas_sdk.commands.commands.allergy import AllergyCommand
|
|
3
3
|
from canvas_sdk.commands.commands.assess import AssessCommand
|
|
4
|
+
from canvas_sdk.commands.commands.chart_section_review import ChartSectionReviewCommand
|
|
4
5
|
from canvas_sdk.commands.commands.close_goal import CloseGoalCommand
|
|
5
6
|
from canvas_sdk.commands.commands.diagnose import DiagnoseCommand
|
|
6
7
|
from canvas_sdk.commands.commands.exam import PhysicalExamCommand
|
|
@@ -39,6 +40,7 @@ __all__ = __exports__ = (
|
|
|
39
40
|
"AdjustPrescriptionCommand",
|
|
40
41
|
"AllergyCommand",
|
|
41
42
|
"AssessCommand",
|
|
43
|
+
"ChartSectionReviewCommand",
|
|
42
44
|
"CloseGoalCommand",
|
|
43
45
|
"DiagnoseCommand",
|
|
44
46
|
"FamilyHistoryCommand",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
from canvas_sdk.commands.base import _BaseCommand
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ChartSectionReviewCommand(_BaseCommand):
|
|
7
|
+
"""A class for managing a Chart Section Review command within a specific note."""
|
|
8
|
+
|
|
9
|
+
class Meta:
|
|
10
|
+
key = "chartSectionReview"
|
|
11
|
+
|
|
12
|
+
class Sections(Enum):
|
|
13
|
+
CONDITIONS = "conditions"
|
|
14
|
+
SURGICAL_HISTORY = "surgical_history"
|
|
15
|
+
MEDICATIONS = "medications"
|
|
16
|
+
FAMILY_HISTORY = "family_histories"
|
|
17
|
+
ALLERGIES = "allergies"
|
|
18
|
+
IMMUNIZATIONS = "immunizations"
|
|
19
|
+
|
|
20
|
+
section: Sections
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
__exports__ = ("ChartSectionReviewCommand",)
|
|
@@ -31,6 +31,9 @@ class PrescribeCommand(_BaseCommand):
|
|
|
31
31
|
prescriber_id: str | None = Field(
|
|
32
32
|
default=None, json_schema_extra={"commands_api_name": "prescriber"}
|
|
33
33
|
)
|
|
34
|
+
supervising_provider_id: str | None = Field(
|
|
35
|
+
default=None, json_schema_extra={"commands_api_name": "supervising_provider"}
|
|
36
|
+
)
|
|
34
37
|
note_to_pharmacist: str | None = None
|
|
35
38
|
|
|
36
39
|
@property
|
canvas_sdk/effects/simple_api.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import re
|
|
2
3
|
from base64 import b64encode
|
|
3
4
|
from collections.abc import Mapping, Sequence
|
|
4
5
|
from http import HTTPStatus
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
7
|
-
from
|
|
8
|
-
|
|
8
|
+
from pydantic_core import InitErrorDetails
|
|
9
|
+
|
|
10
|
+
from canvas_sdk.effects import Effect, EffectType, _BaseEffect
|
|
9
11
|
|
|
10
12
|
JSON = Mapping[str, "JSON"] | Sequence["JSON"] | int | float | str | bool | None
|
|
11
13
|
|
|
@@ -83,10 +85,67 @@ class HTMLResponse(Response):
|
|
|
83
85
|
super().__init__(content.encode(), status_code, headers, content_type="text/html")
|
|
84
86
|
|
|
85
87
|
|
|
88
|
+
class AcceptConnection(_BaseEffect):
|
|
89
|
+
"""AcceptConnection effect."""
|
|
90
|
+
|
|
91
|
+
class Meta:
|
|
92
|
+
effect_type = EffectType.SIMPLE_API_WEBSOCKET_ACCEPT
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class DenyConnection(_BaseEffect):
|
|
96
|
+
"""DenyConnection effect."""
|
|
97
|
+
|
|
98
|
+
message: str | None = None
|
|
99
|
+
|
|
100
|
+
class Meta:
|
|
101
|
+
effect_type = EffectType.SIMPLE_API_WEBSOCKET_DENY
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def values(self) -> dict[str, Any]:
|
|
105
|
+
"""Make the payload."""
|
|
106
|
+
return {"message": self.message} if self.message else {}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class Broadcast(_BaseEffect):
|
|
110
|
+
"""Broadcast effect."""
|
|
111
|
+
|
|
112
|
+
class Meta:
|
|
113
|
+
effect_type = EffectType.SIMPLE_API_WEBSOCKET_BROADCAST
|
|
114
|
+
|
|
115
|
+
channel: str
|
|
116
|
+
message: dict[str, Any]
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def values(self) -> dict[str, Any]:
|
|
120
|
+
"""Make the payload."""
|
|
121
|
+
return {"channel": self.channel, "message": self.message}
|
|
122
|
+
|
|
123
|
+
def is_valid_channel_name(self) -> bool:
|
|
124
|
+
"""Check if the channel name is valid."""
|
|
125
|
+
return re.fullmatch(r"\w+", self.channel) is not None
|
|
126
|
+
|
|
127
|
+
def _get_error_details(self, method: Any) -> list[InitErrorDetails]:
|
|
128
|
+
errors = super()._get_error_details(method)
|
|
129
|
+
|
|
130
|
+
if not self.is_valid_channel_name():
|
|
131
|
+
errors.append(
|
|
132
|
+
self._create_error_detail(
|
|
133
|
+
"value",
|
|
134
|
+
"Invalid channel name. Channel name must be alphanumeric and can contain underscores.",
|
|
135
|
+
self.channel,
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return errors
|
|
140
|
+
|
|
141
|
+
|
|
86
142
|
__exports__ = (
|
|
87
143
|
"JSON",
|
|
88
144
|
"Response",
|
|
89
145
|
"JSONResponse",
|
|
90
146
|
"PlainTextResponse",
|
|
91
147
|
"HTMLResponse",
|
|
148
|
+
"AcceptConnection",
|
|
149
|
+
"DenyConnection",
|
|
150
|
+
"Broadcast",
|
|
92
151
|
)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import traceback
|
|
2
|
+
from abc import ABC
|
|
3
|
+
from functools import cached_property
|
|
4
|
+
from typing import ClassVar
|
|
5
|
+
|
|
6
|
+
import sentry_sdk
|
|
7
|
+
|
|
8
|
+
from canvas_sdk.effects import Effect
|
|
9
|
+
from canvas_sdk.effects.simple_api import AcceptConnection, DenyConnection
|
|
10
|
+
from canvas_sdk.events import Event, EventType
|
|
11
|
+
from canvas_sdk.handlers.base import BaseHandler
|
|
12
|
+
from logger import log
|
|
13
|
+
|
|
14
|
+
from .exceptions import AuthenticationError, InvalidCredentialsError
|
|
15
|
+
from .tools import CaseInsensitiveMultiDict, separate_headers
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WebSocket:
|
|
19
|
+
"""WebSocket class for incoming requests to the WebSocketAPI."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, event: Event) -> None:
|
|
22
|
+
self.channel = event.context["channel_name"]
|
|
23
|
+
self.headers = CaseInsensitiveMultiDict(separate_headers(event.context["headers"]))
|
|
24
|
+
self.api_key = self.headers.get("authorization")
|
|
25
|
+
self.logged_in_user = (
|
|
26
|
+
{
|
|
27
|
+
"id": self.headers.get("canvas-logged-in-user-id", ""),
|
|
28
|
+
"type": self.headers.get("canvas-logged-in-user-type", ""),
|
|
29
|
+
}
|
|
30
|
+
if "canvas-logged-in-user-id" in self.headers
|
|
31
|
+
else None
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class WebSocketAPI(BaseHandler, ABC):
|
|
36
|
+
"""Abstract base class for WebSocket APIs."""
|
|
37
|
+
|
|
38
|
+
RESPONDS_TO: ClassVar[list[str]] = [
|
|
39
|
+
EventType.Name(EventType.SIMPLE_API_WEBSOCKET_AUTHENTICATE),
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
@cached_property
|
|
43
|
+
def websocket(self) -> WebSocket:
|
|
44
|
+
"""Return the WebSocket object for the event."""
|
|
45
|
+
return WebSocket(self.event)
|
|
46
|
+
|
|
47
|
+
def compute(self) -> list[Effect]:
|
|
48
|
+
"""Handle WebSocket authenticate event."""
|
|
49
|
+
try:
|
|
50
|
+
if self.event.type == EventType.SIMPLE_API_WEBSOCKET_AUTHENTICATE:
|
|
51
|
+
return self._authenticate()
|
|
52
|
+
else:
|
|
53
|
+
raise AssertionError(f"Cannot handle event type {EventType.Name(self.event.type)}")
|
|
54
|
+
except Exception as exception:
|
|
55
|
+
for error_line_with_newlines in traceback.format_exception(exception):
|
|
56
|
+
for error_line in error_line_with_newlines.split("\n"):
|
|
57
|
+
log.error(error_line)
|
|
58
|
+
|
|
59
|
+
sentry_sdk.capture_exception(exception)
|
|
60
|
+
|
|
61
|
+
return [DenyConnection(message="Internal server error").apply()]
|
|
62
|
+
|
|
63
|
+
def _authenticate(self) -> list[Effect]:
|
|
64
|
+
"""Authenticate the WebSocket request."""
|
|
65
|
+
try:
|
|
66
|
+
if self.authenticate():
|
|
67
|
+
return [AcceptConnection().apply()]
|
|
68
|
+
else:
|
|
69
|
+
raise InvalidCredentialsError
|
|
70
|
+
|
|
71
|
+
except AuthenticationError:
|
|
72
|
+
return [DenyConnection(message="Unauthorized").apply()]
|
|
73
|
+
|
|
74
|
+
def authenticate(self) -> bool:
|
|
75
|
+
"""Override to implement authentication."""
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
__exports__ = ("WebSocketAPI", "WebSocket")
|
canvas_sdk/value_set/custom.py
CHANGED
|
@@ -40656,4 +40656,4 @@ class UrinaryRetention(ValueSet):
|
|
|
40656
40656
|
}
|
|
40657
40657
|
|
|
40658
40658
|
|
|
40659
|
-
__exports__ = get_overrides(locals()
|
|
40659
|
+
__exports__ = get_overrides(locals())
|