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.

Files changed (42) hide show
  1. {canvas-0.35.1.dist-info → canvas-0.37.0.dist-info}/METADATA +1 -1
  2. {canvas-0.35.1.dist-info → canvas-0.37.0.dist-info}/RECORD +42 -34
  3. canvas_cli/apps/plugin/plugin.py +3 -2
  4. canvas_cli/utils/validators/manifest_schema.py +15 -1
  5. canvas_generated/messages/effects_pb2.py +2 -2
  6. canvas_generated/messages/effects_pb2.pyi +8 -0
  7. canvas_generated/messages/events_pb2.py +2 -2
  8. canvas_generated/messages/events_pb2.pyi +26 -0
  9. canvas_sdk/caching/__init__.py +1 -0
  10. canvas_sdk/caching/base.py +127 -0
  11. canvas_sdk/caching/client.py +24 -0
  12. canvas_sdk/caching/exceptions.py +21 -0
  13. canvas_sdk/caching/plugins.py +20 -0
  14. canvas_sdk/caching/utils.py +28 -0
  15. canvas_sdk/commands/__init__.py +2 -0
  16. canvas_sdk/commands/commands/chart_section_review.py +23 -0
  17. canvas_sdk/commands/commands/prescribe.py +3 -0
  18. canvas_sdk/effects/launch_modal.py +1 -0
  19. canvas_sdk/effects/simple_api.py +61 -2
  20. canvas_sdk/handlers/simple_api/websocket.py +79 -0
  21. canvas_sdk/value_set/custom.py +1 -1
  22. canvas_sdk/value_set/v2022/allergy.py +1 -1
  23. canvas_sdk/value_set/v2022/assessment.py +1 -1
  24. canvas_sdk/value_set/v2022/communication.py +1 -1
  25. canvas_sdk/value_set/v2022/condition.py +1 -1
  26. canvas_sdk/value_set/v2022/device.py +1 -1
  27. canvas_sdk/value_set/v2022/diagnostic_study.py +1 -1
  28. canvas_sdk/value_set/v2022/encounter.py +1 -1
  29. canvas_sdk/value_set/v2022/immunization.py +1 -1
  30. canvas_sdk/value_set/v2022/individual_characteristic.py +1 -1
  31. canvas_sdk/value_set/v2022/intervention.py +1 -1
  32. canvas_sdk/value_set/v2022/laboratory_test.py +1 -1
  33. canvas_sdk/value_set/v2022/medication.py +1 -1
  34. canvas_sdk/value_set/v2022/physical_exam.py +1 -1
  35. canvas_sdk/value_set/v2022/procedure.py +1 -1
  36. plugin_runner/plugin_runner.py +15 -1
  37. plugin_runner/sandbox.py +5 -0
  38. protobufs/canvas_generated/messages/effects.proto +6 -0
  39. protobufs/canvas_generated/messages/events.proto +14 -12
  40. settings.py +28 -0
  41. {canvas-0.35.1.dist-info → canvas-0.37.0.dist-info}/WHEEL +0 -0
  42. {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__ = ()
@@ -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
@@ -17,6 +17,7 @@ class LaunchModalEffect(_BaseEffect):
17
17
  NEW_WINDOW = "new_window"
18
18
  RIGHT_CHART_PANE = "right_chart_pane"
19
19
  RIGHT_CHART_PANE_LARGE = "right_chart_pane_large"
20
+ PAGE = "page"
20
21
 
21
22
  url: str | None = None
22
23
  content: str | None = None
@@ -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 canvas_generated.messages.effects_pb2 import EffectType
8
- from canvas_sdk.effects import Effect, _BaseEffect
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")
@@ -1058,4 +1058,4 @@ class DiabetesOtherClassConditionSuspect(ValueSet):
1058
1058
  }
1059
1059
 
1060
1060
 
1061
- __exports__ = get_overrides(locals().copy())
1061
+ __exports__ = get_overrides(locals())
@@ -234,4 +234,4 @@ class StatinAllergen(ValueSet):
234
234
  }
235
235
 
236
236
 
237
- __exports__ = get_overrides(locals().copy())
237
+ __exports__ = get_overrides(locals())
@@ -217,4 +217,4 @@ class HistoryOfHipFractureInParent(ValueSet):
217
217
  }
218
218
 
219
219
 
220
- __exports__ = get_overrides(locals().copy())
220
+ __exports__ = get_overrides(locals())
@@ -327,4 +327,4 @@ class ConsultantReport(ValueSet):
327
327
  }
328
328
 
329
329
 
330
- __exports__ = get_overrides(locals().copy())
330
+ __exports__ = get_overrides(locals())
@@ -40656,4 +40656,4 @@ class UrinaryRetention(ValueSet):
40656
40656
  }
40657
40657
 
40658
40658
 
40659
- __exports__ = get_overrides(locals().copy())
40659
+ __exports__ = get_overrides(locals())
@@ -176,4 +176,4 @@ class CardiacPacer(ValueSet):
176
176
  }
177
177
 
178
178
 
179
- __exports__ = get_overrides(locals().copy())
179
+ __exports__ = get_overrides(locals())
@@ -4969,4 +4969,4 @@ class DexaDualEnergyXrayAbsorptiometry_BoneDensityForUrologyCare(ValueSet):
4969
4969
  }
4970
4970
 
4971
4971
 
4972
- __exports__ = get_overrides(locals().copy())
4972
+ __exports__ = get_overrides(locals())
@@ -2566,4 +2566,4 @@ class HospitalServicesForUrologyCare(ValueSet):
2566
2566
  }
2567
2567
 
2568
2568
 
2569
- __exports__ = get_overrides(locals().copy())
2569
+ __exports__ = get_overrides(locals())
@@ -343,4 +343,4 @@ class PneumococcalPolysaccharide23Vaccine(ValueSet):
343
343
  }
344
344
 
345
345
 
346
- __exports__ = get_overrides(locals().copy())
346
+ __exports__ = get_overrides(locals())
@@ -315,4 +315,4 @@ class White(ValueSet):
315
315
  }
316
316
 
317
317
 
318
- __exports__ = get_overrides(locals().copy())
318
+ __exports__ = get_overrides(locals())
@@ -1334,4 +1334,4 @@ class ReferralsWhereWeightAssessmentMayOccur(ValueSet):
1334
1334
  }
1335
1335
 
1336
1336
 
1337
- __exports__ = get_overrides(locals().copy())
1337
+ __exports__ = get_overrides(locals())
@@ -1252,4 +1252,4 @@ class HumanImmunodeficiencyVirusHivLaboratoryTestCodesAbAndAg(ValueSet):
1252
1252
  }
1253
1253
 
1254
1254
 
1255
- __exports__ = get_overrides(locals().copy())
1255
+ __exports__ = get_overrides(locals())
@@ -5132,4 +5132,4 @@ class MedicationsForBelowNormalBmi(ValueSet):
5132
5132
  }
5133
5133
 
5134
5134
 
5135
- __exports__ = get_overrides(locals().copy())
5135
+ __exports__ = get_overrides(locals())
@@ -203,4 +203,4 @@ class BmiRatio(ValueSet):
203
203
  }
204
204
 
205
205
 
206
- __exports__ = get_overrides(locals().copy())
206
+ __exports__ = get_overrides(locals())
@@ -4041,4 +4041,4 @@ class FluorideVarnishApplicationForChildren(ValueSet):
4041
4041
  }
4042
4042
 
4043
4043
 
4044
- __exports__ = get_overrides(locals().copy())
4044
+ __exports__ = get_overrides(locals())