canvas 0.22.0__py3-none-any.whl → 0.22.1__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: canvas
3
- Version: 0.22.0
3
+ Version: 0.22.1
4
4
  Summary: SDK to customize event-driven actions in your Canvas instance
5
5
  Author-email: Canvas Team <engineering@canvasmedical.com>
6
6
  License-Expression: MIT
@@ -150,9 +150,9 @@ canvas_sdk/commands/commands/vitals.py,sha256=jidkqVNpEqVewMBHztY0p7xkfk9xdBNvGS
150
150
  canvas_sdk/commands/commands/questionnaire/__init__.py,sha256=huAj5B93JW_tqur-s1w-mzZia_QPIYvbBngHgCyVFDk,3635
151
151
  canvas_sdk/commands/commands/questionnaire/question.py,sha256=h0AyCfMqpIMUMFZ-4xCL-vEbrhkgTWnOGVj5MTOKZjw,4334
152
152
  canvas_sdk/commands/tests/test_base_command.py,sha256=k5I4psjNX4VgfC4XWHuG0sJdgUa58DJP7WQQ3QncMtQ,2667
153
- canvas_sdk/commands/tests/test_utils.py,sha256=Jrkbz2xltrjUVYRdIyNaKdq5tCtGQ4vygic7hpBp6Ek,12157
153
+ canvas_sdk/commands/tests/test_utils.py,sha256=4XBQLJiWqs3frqgofA9I9o2wvi8Og2378QkcS4kgYpI,12275
154
154
  canvas_sdk/commands/tests/protocol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
155
- canvas_sdk/commands/tests/protocol/tests.py,sha256=e0fiJUKsfi0aYSlaTdrMHybZntC7oBdaHks9PSrMuhU,2168
155
+ canvas_sdk/commands/tests/protocol/tests.py,sha256=ApG6R66e7-PyMTAPcVHVujvdEmCOEFVlcORtYs4KD7c,2651
156
156
  canvas_sdk/commands/tests/schema/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
157
157
  canvas_sdk/commands/tests/schema/tests.py,sha256=rSu146RouMrPLv0G-E6QtgQhG2vkUCCk1M3Baa6fXBg,3701
158
158
  canvas_sdk/commands/tests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -167,7 +167,7 @@ canvas_sdk/effects/show_button.py,sha256=JnW9nM8S_GUXIOufs-uef3pg0HPDxSbF0l51Wh1
167
167
  canvas_sdk/effects/banner_alert/__init__.py,sha256=PcMmOjLHP_MOZiP8157JkTdcO4mZTn-kFcRylOB9AK0,209
168
168
  canvas_sdk/effects/banner_alert/add_banner_alert.py,sha256=W4Fv9IHGCWvVtFiH3s68JBRpUb8b3xsncGt2WfgMFc0,1681
169
169
  canvas_sdk/effects/banner_alert/remove_banner_alert.py,sha256=PK_LzXBQ-rOH3fX182ISyutu4W69xyGgMX-1G4g4XhY,613
170
- canvas_sdk/effects/banner_alert/tests.py,sha256=DoCQ9ZfpcUBne-Le-zCCOTjzF_WHyuiay-FCex4aHJ0,9665
170
+ canvas_sdk/effects/banner_alert/tests.py,sha256=ddSIiyD1sIzWbZmJEHcSN8cXn2R-0-CqD7jyDzqZOzE,10179
171
171
  canvas_sdk/effects/billing_line_item/__init__.py,sha256=IiuClemwdeMNku_uyphQ8OIE7xLiNg5sgPpTvdU3uJw,393
172
172
  canvas_sdk/effects/billing_line_item/add_billing_line_item.py,sha256=TJp3KKhpDK48T9kxvD7rOttFx6OLb6wfS5Y2Pmg9sGk,1019
173
173
  canvas_sdk/effects/billing_line_item/remove_billing_line_item.py,sha256=tUCv9NVTVeDixkJM_TNN9TcjUvefR0GA3IOzVVDdD6E,585
@@ -266,8 +266,8 @@ plugin_runner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
266
266
  plugin_runner/authentication.py,sha256=SDPso2AogtLAV_H0LuMDp99IMZuF3oTq-Q_AXAvJ8uc,1116
267
267
  plugin_runner/aws_headers.py,sha256=DenX_nAMVhXMJZw88PLZbqJsi5_XriNtr3jE-eJqHY4,2773
268
268
  plugin_runner/exceptions.py,sha256=YnRZiQVzbU3HrVlmEXLje_np99009YnhTRVHHyBCtqc,433
269
- plugin_runner/installation.py,sha256=ix-GvllnnQMSwbs9r0FxgWiyd0GsTsaOIrdgYytfxBo,7596
270
- plugin_runner/plugin_runner.py,sha256=XfNbZLvp04GoVkOCj1SYUuuOmqWV18GJslFoshRUd5E,17173
269
+ plugin_runner/installation.py,sha256=-TntCAveju5vWrKRLnIxy9xn3pnU3goo5dT4tGs-85s,7537
270
+ plugin_runner/plugin_runner.py,sha256=8f0wJp1QirzKQjwmIZzVgTwtx0OxVKY4-xTiBxhFJHQ,18023
271
271
  plugin_runner/sandbox.py,sha256=P64SKpKZdoJwk2NsirGRKq7xjygBtvdipIAVSD5roY8,14509
272
272
  plugin_runner/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
273
273
  plugin_runner/tests/test_application.py,sha256=e1R2YagMRD96gZALx-Zra-e-sR3SiP7cIpI6pheZnUc,2427
@@ -340,7 +340,7 @@ protobufs/canvas_generated/messages/plugins.proto,sha256=oNainUPWFYQjgCX7bJEPI9_
340
340
  protobufs/canvas_generated/services/plugin_runner.proto,sha256=doadBKn5k4xAtOgR-q_pEvW4yzxpUaHNOowMG6CL5GY,304
341
341
  pubsub/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
342
342
  pubsub/pubsub.py,sha256=pyTW0JU8mtaqiAV6g6xjZwel1CVy2EonPMU-_vkmhUM,1044
343
- canvas-0.22.0.dist-info/METADATA,sha256=q6j_FoE3wxCTZldQsWQolKZmxqi23_NU90Evpdqcebs,4375
344
- canvas-0.22.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
345
- canvas-0.22.0.dist-info/entry_points.txt,sha256=0Vs_9GmTVUNniH6eDBlRPgofmADMV4BES6Ao26M4AbM,47
346
- canvas-0.22.0.dist-info/RECORD,,
343
+ canvas-0.22.1.dist-info/METADATA,sha256=5XB3DdvBg3cfMCwFEHZE6vhAZPhk0u93rWhD0SVhd0E,4375
344
+ canvas-0.22.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
345
+ canvas-0.22.1.dist-info/entry_points.txt,sha256=0Vs_9GmTVUNniH6eDBlRPgofmADMV4BES6Ao26M4AbM,47
346
+ canvas-0.22.1.dist-info/RECORD,,
@@ -49,7 +49,16 @@ def write_and_install_protocol_and_clean_up(
49
49
  f"Loading plugin '{plugin_name}",
50
50
  )
51
51
  install_plugin(plugin_name, token)
52
- message_received_event.wait(timeout=5.0)
52
+ message_received_event.wait(timeout=15.0)
53
+
54
+ # unfortunately sometimes the log websocket just doesn't return any
55
+ # messages, so asserting on the state of the timeout here causes failures
56
+ # even though the delay itself will cause the waiting test to pass (because
57
+ # the plugin has been loaded).
58
+ # timeout_not_hit = message_received_event.wait(timeout=15.0)
59
+ # if not timeout_not_hit:
60
+ # ws.close()
61
+ # assert timeout_not_hit, f"plugin loading message timeout hit: Loading plugin '{plugin_name}"
53
62
 
54
63
  yield
55
64
 
@@ -367,6 +367,9 @@ def wait_for_log(
367
367
  thread = threading.Thread(target=ws.run_forever)
368
368
  thread.start()
369
369
 
370
- connected_event.wait(timeout=5.0)
370
+ timeout_not_hit = connected_event.wait(timeout=5.0)
371
+ if not timeout_not_hit:
372
+ ws.close()
373
+ assert timeout_not_hit, "connection timeout hit"
371
374
 
372
375
  return message_received_event, thread, ws
@@ -107,8 +107,16 @@ class Protocol(BaseProtocol):
107
107
  )
108
108
  response.raise_for_status()
109
109
 
110
- message_received_event.wait(timeout=5.0)
111
-
110
+ message_received_event.wait(timeout=15.0)
111
+
112
+ # unfortunately sometimes the log websocket just doesn't return any
113
+ # messages, so asserting on the state of the timeout here causes failures
114
+ # even though the delay itself will cause the waiting test to pass (because
115
+ # the plugin has been loaded).
116
+ # timeout_not_hit = message_received_event.wait(timeout=15.0)
117
+ # if not timeout_not_hit:
118
+ # ws.close()
119
+ # assert timeout_not_hit, f"plugin loading message timeout hit: Loading plugin '{plugin_name}"
112
120
  yield
113
121
 
114
122
  ws.close()
@@ -137,7 +137,7 @@ def download_plugin(plugin_package: str) -> Generator[Path, None, None]:
137
137
  def install_plugin(plugin_name: str, attributes: PluginAttributes) -> None:
138
138
  """Install the given Plugin's package into the runtime."""
139
139
  try:
140
- log.info(f'Installing plugin "{plugin_name}"')
140
+ log.info(f'Installing plugin "{plugin_name}", version {attributes["version"]}')
141
141
 
142
142
  plugin_installation_path = Path(PLUGIN_DIRECTORY) / plugin_name
143
143
 
@@ -224,7 +224,6 @@ def install_plugins() -> None:
224
224
 
225
225
  for plugin_name, attributes in enabled_plugins().items():
226
226
  try:
227
- log.info(f'Installing plugin "{plugin_name}", version {attributes["version"]}')
228
227
  install_plugin(plugin_name, attributes)
229
228
  except PluginInstallationError:
230
229
  disable_plugin(plugin_name)
@@ -13,6 +13,8 @@ from typing import Any, TypedDict
13
13
 
14
14
  import grpc
15
15
  import redis.asyncio as redis
16
+ from asgiref.sync import sync_to_async
17
+ from django.db import connections
16
18
 
17
19
  from canvas_generated.messages.effects_pb2 import EffectType
18
20
  from canvas_generated.messages.plugins_pb2 import ReloadPluginsRequest, ReloadPluginsResponse
@@ -30,6 +32,7 @@ from plugin_runner.installation import install_plugins
30
32
  from plugin_runner.sandbox import Sandbox
31
33
  from settings import (
32
34
  CHANNEL_NAME,
35
+ IS_TESTING,
33
36
  MANIFEST_FILE_NAME,
34
37
  PLUGIN_DIRECTORY,
35
38
  REDIS_ENDPOINT,
@@ -103,6 +106,25 @@ class PluginManifest(TypedDict):
103
106
  readme: str
104
107
 
105
108
 
109
+ @sync_to_async
110
+ def reconnect_if_needed() -> None:
111
+ """
112
+ Reconnect to the database if the connection has been closed.
113
+
114
+ NOTE: Django database functions are not async-safe and must be called
115
+ via e.g. sync_to_async. They will silently fail and block the handler
116
+ if called in an async context directly.
117
+ """
118
+ if IS_TESTING:
119
+ return
120
+
121
+ connection = connections["default"]
122
+
123
+ if not connection.is_usable():
124
+ log.debug("Connection was unusable, reconnecting...")
125
+ connection.connect()
126
+
127
+
106
128
  class PluginRunner(PluginRunnerServicer):
107
129
  """This process runs provided plugins that register interest in incoming events."""
108
130
 
@@ -118,6 +140,11 @@ class PluginRunner(PluginRunnerServicer):
118
140
  event_name = event.name
119
141
  relevant_plugins = EVENT_HANDLER_MAP[event_name]
120
142
 
143
+ log.debug(f"Processing {relevant_plugins} for {event_name}")
144
+
145
+ if relevant_plugins:
146
+ await reconnect_if_needed()
147
+
121
148
  if event_type in [EventType.PLUGIN_CREATED, EventType.PLUGIN_UPDATED]:
122
149
  plugin_name = event.target.id
123
150
  # filter only for the plugin(s) that were created/updated
@@ -126,6 +153,8 @@ class PluginRunner(PluginRunnerServicer):
126
153
  effect_list = []
127
154
 
128
155
  for plugin_name in relevant_plugins:
156
+ log.debug(f"Processing {plugin_name}")
157
+
129
158
  plugin = LOADED_PLUGINS[plugin_name]
130
159
  handler_class = plugin["class"]
131
160
  base_plugin_name = plugin_name.split(":")[0]
@@ -142,7 +171,7 @@ class PluginRunner(PluginRunnerServicer):
142
171
  )
143
172
 
144
173
  compute_start_time = time.time()
145
- _effects = await asyncio.get_running_loop().run_in_executor(None, handler.compute)
174
+ _effects = await sync_to_async(handler.compute)()
146
175
  effects = [
147
176
  Effect(
148
177
  type=effect.type,
@@ -166,9 +195,12 @@ class PluginRunner(PluginRunnerServicer):
166
195
  tags={"plugin": plugin_name},
167
196
  )
168
197
  except Exception as e:
198
+ log.error(f"Encountered exception in plugin {plugin_name}:")
199
+
169
200
  for error_line_with_newlines in traceback.format_exception(e):
170
201
  for error_line in error_line_with_newlines.split("\n"):
171
202
  log.error(error_line)
203
+
172
204
  continue
173
205
 
174
206
  effect_list += effects
@@ -204,7 +236,7 @@ async def synchronize_plugins(run_once: bool = False) -> None:
204
236
  """
205
237
  log.info(f'synchronize_plugins: listening for messages on pubsub channel "{CHANNEL_NAME}"')
206
238
 
207
- client, pubsub = get_client()
239
+ _, pubsub = get_client()
208
240
  await pubsub.psubscribe(CHANNEL_NAME)
209
241
 
210
242
  while True: