canvas 0.35.1__py3-none-any.whl → 0.36.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.36.0.dist-info}/METADATA +1 -1
- {canvas-0.35.1.dist-info → canvas-0.36.0.dist-info}/RECORD +41 -33
- 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 +14 -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/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 +1 -0
- protobufs/canvas_generated/messages/effects.proto +6 -0
- protobufs/canvas_generated/messages/events.proto +8 -12
- settings.py +28 -0
- {canvas-0.35.1.dist-info → canvas-0.36.0.dist-info}/WHEEL +0 -0
- {canvas-0.35.1.dist-info → canvas-0.36.0.dist-info}/entry_points.txt +0 -0
|
@@ -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",)
|
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())
|
plugin_runner/plugin_runner.py
CHANGED
|
@@ -32,6 +32,7 @@ from canvas_generated.services.plugin_runner_pb2_grpc import (
|
|
|
32
32
|
from canvas_sdk.effects import Effect
|
|
33
33
|
from canvas_sdk.effects.simple_api import Response
|
|
34
34
|
from canvas_sdk.events import Event, EventRequest, EventResponse, EventType
|
|
35
|
+
from canvas_sdk.handlers.simple_api.websocket import DenyConnection
|
|
35
36
|
from canvas_sdk.protocols import ClinicalQualityMeasure
|
|
36
37
|
from canvas_sdk.utils import metrics
|
|
37
38
|
from canvas_sdk.utils.metrics import measured
|
|
@@ -180,7 +181,11 @@ class PluginRunner(PluginRunnerServicer):
|
|
|
180
181
|
plugin_name = event.target.id
|
|
181
182
|
# filter only for the plugin(s) that were created/updated
|
|
182
183
|
relevant_plugins = [p for p in relevant_plugins if p.startswith(f"{plugin_name}:")]
|
|
183
|
-
elif event_type in {
|
|
184
|
+
elif event_type in {
|
|
185
|
+
EventType.SIMPLE_API_AUTHENTICATE,
|
|
186
|
+
EventType.SIMPLE_API_REQUEST,
|
|
187
|
+
EventType.SIMPLE_API_WEBSOCKET_AUTHENTICATE,
|
|
188
|
+
}:
|
|
184
189
|
# The target plugin's name will be part of the home-app URL path, so other plugins that
|
|
185
190
|
# respond to SimpleAPI request events are not relevant
|
|
186
191
|
plugin_name = event.context["plugin_name"]
|
|
@@ -267,6 +272,15 @@ class PluginRunner(PluginRunnerServicer):
|
|
|
267
272
|
f" {event.context['path']}"
|
|
268
273
|
)
|
|
269
274
|
effect_list = [Response(status_code=HTTPStatus.INTERNAL_SERVER_ERROR).apply()]
|
|
275
|
+
if event.type == EventType.SIMPLE_API_WEBSOCKET_AUTHENTICATE:
|
|
276
|
+
if len(relevant_plugin_handlers) == 0:
|
|
277
|
+
effect_list = [DenyConnection().apply()]
|
|
278
|
+
elif len(relevant_plugin_handlers) > 1:
|
|
279
|
+
log.error(
|
|
280
|
+
f"Multiple handlers responded to {EventType.Name(EventType.SIMPLE_API_WEBSOCKET_AUTHENTICATE)}"
|
|
281
|
+
f" {event.context['channel']}"
|
|
282
|
+
)
|
|
283
|
+
effect_list = [DenyConnection().apply()]
|
|
270
284
|
|
|
271
285
|
# Don't log anything if a plugin handler didn't actually run.
|
|
272
286
|
if relevant_plugins:
|
plugin_runner/sandbox.py
CHANGED
|
@@ -233,6 +233,8 @@ enum EffectType {
|
|
|
233
233
|
COMMIT_ADJUST_PRESCRIPTION_COMMAND = 1303;
|
|
234
234
|
ENTER_IN_ERROR_ADJUST_PRESCRIPTION_COMMAND = 1304;
|
|
235
235
|
|
|
236
|
+
ORIGINATE_CHART_SECTION_REVIEW_COMMAND = 1400;
|
|
237
|
+
|
|
236
238
|
SHOW_ACTION_BUTTON = 1000;
|
|
237
239
|
|
|
238
240
|
PATIENT_PORTAL__FORM_RESULT = 2000;
|
|
@@ -260,6 +262,9 @@ enum EffectType {
|
|
|
260
262
|
LAUNCH_MODAL = 3000;
|
|
261
263
|
|
|
262
264
|
SIMPLE_API_RESPONSE = 4000;
|
|
265
|
+
SIMPLE_API_WEBSOCKET_ACCEPT = 4001;
|
|
266
|
+
SIMPLE_API_WEBSOCKET_DENY = 4002;
|
|
267
|
+
SIMPLE_API_WEBSOCKET_BROADCAST = 4003;
|
|
263
268
|
|
|
264
269
|
UPDATE_USER = 5000;
|
|
265
270
|
|
|
@@ -268,6 +273,7 @@ enum EffectType {
|
|
|
268
273
|
CREATE_SCHEDULE_EVENT = 6002;
|
|
269
274
|
|
|
270
275
|
CREATE_PATIENT = 6003;
|
|
276
|
+
|
|
271
277
|
}
|
|
272
278
|
|
|
273
279
|
message Effect {
|
|
@@ -244,18 +244,12 @@ enum EventType {
|
|
|
244
244
|
CANCEL_PRESCRIPTION__SELECTED_PRESCRIPTION__PRE_SEARCH = 8012;
|
|
245
245
|
CANCEL_PRESCRIPTION__SELECTED_PRESCRIPTION__POST_SEARCH = 8013;
|
|
246
246
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
// CHART_SECTION_REVIEW_COMMAND__PRE_DELETE = 9006;
|
|
254
|
-
// CHART_SECTION_REVIEW_COMMAND__POST_DELETE = 9007;
|
|
255
|
-
// CHART_SECTION_REVIEW_COMMAND__PRE_ENTER_IN_ERROR = 9008;
|
|
256
|
-
// CHART_SECTION_REVIEW_COMMAND__POST_ENTER_IN_ERROR = 9009;
|
|
257
|
-
// CHART_SECTION_REVIEW_COMMAND__PRE_EXECUTE_ACTION = 9010;
|
|
258
|
-
// CHART_SECTION_REVIEW_COMMAND__POST_EXECUTE_ACTION = 9011;
|
|
247
|
+
CHART_SECTION_REVIEW_COMMAND__PRE_ORIGINATE = 9000;
|
|
248
|
+
CHART_SECTION_REVIEW_COMMAND__POST_ORIGINATE = 9001;
|
|
249
|
+
CHART_SECTION_REVIEW_COMMAND__PRE_ENTER_IN_ERROR = 9002;
|
|
250
|
+
CHART_SECTION_REVIEW_COMMAND__POST_ENTER_IN_ERROR = 9003;
|
|
251
|
+
CHART_SECTION_REVIEW_COMMAND__PRE_EXECUTE_ACTION = 9004;
|
|
252
|
+
CHART_SECTION_REVIEW_COMMAND__POST_EXECUTE_ACTION = 9005;
|
|
259
253
|
|
|
260
254
|
|
|
261
255
|
CLIPBOARD_COMMAND__PRE_ORIGINATE = 53000;
|
|
@@ -1093,6 +1087,8 @@ enum EventType {
|
|
|
1093
1087
|
|
|
1094
1088
|
SIMPLE_API_AUTHENTICATE = 130000;
|
|
1095
1089
|
SIMPLE_API_REQUEST = 130001;
|
|
1090
|
+
SIMPLE_API_WEBSOCKET_AUTHENTICATE = 130002;
|
|
1091
|
+
|
|
1096
1092
|
}
|
|
1097
1093
|
|
|
1098
1094
|
message Event {
|