canvas 0.53.2__py3-none-any.whl → 0.55.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.

@@ -3,12 +3,14 @@ from typing import Any
3
3
 
4
4
  from django.template import Context, Template
5
5
 
6
- from canvas_sdk.utils.plugins import plugin_only
6
+ from canvas_sdk.utils.plugins import plugin_context
7
7
 
8
8
 
9
- @plugin_only
9
+ @plugin_context
10
10
  def render_to_string(
11
- template_name: str, context: dict[str, Any] | None = None, **kwargs: Any
11
+ template_name: str,
12
+ context: dict[str, Any] | None = None,
13
+ **kwargs: Any,
12
14
  ) -> str | None:
13
15
  """Load a template and render it with the given context.
14
16
 
@@ -8,19 +8,39 @@ from canvas_sdk.utils.metrics import measured
8
8
  from settings import PLUGIN_DIRECTORY
9
9
 
10
10
 
11
+ def find_plugin_ancestor(frame: FrameType | None, max_depth: int = 10) -> FrameType | None:
12
+ """
13
+ Recurse backwards to find any plugin ancestor of this frame.
14
+ """
15
+ parent_frame = frame.f_back if frame else None
16
+
17
+ if not parent_frame:
18
+ return None
19
+
20
+ if max_depth == 0:
21
+ return None
22
+
23
+ if "__is_plugin__" in parent_frame.f_globals:
24
+ return parent_frame
25
+
26
+ return find_plugin_ancestor(frame=parent_frame, max_depth=max_depth - 1)
27
+
28
+
11
29
  @measured
12
- def plugin_only(func: Callable[..., Any]) -> Callable[..., Any]:
30
+ def plugin_context(func: Callable[..., Any]) -> Callable[..., Any]:
13
31
  """Decorator to restrict a function's execution to plugins only."""
14
32
 
15
33
  def wrapper(*args: Any, **kwargs: Any) -> Any:
16
- current_frame = inspect.currentframe()
17
- caller = current_frame.f_back if current_frame else None
34
+ plugin_frame = find_plugin_ancestor(inspect.currentframe())
18
35
 
19
- if not caller or "__is_plugin__" not in caller.f_globals:
20
- return None
36
+ if not plugin_frame:
37
+ raise RuntimeError(
38
+ "Method that expected plugin context was called from outside a plugin."
39
+ )
21
40
 
22
- plugin_name = caller.f_globals["__name__"].split(".")[0]
41
+ plugin_name = plugin_frame.f_globals["__name__"].split(".")[0]
23
42
  plugin_dir = Path(PLUGIN_DIRECTORY) / plugin_name
43
+
24
44
  kwargs["plugin_name"] = plugin_name
25
45
  kwargs["plugin_dir"] = plugin_dir.resolve()
26
46
 
@@ -30,24 +50,17 @@ def plugin_only(func: Callable[..., Any]) -> Callable[..., Any]:
30
50
 
31
51
 
32
52
  @measured
33
- def is_plugin_caller(depth: int = 10, frame: FrameType | None = None) -> tuple[bool, str | None]:
53
+ def is_plugin_caller() -> tuple[bool, str | None]:
34
54
  """Check if a function is called from a plugin."""
35
- current_frame = frame or inspect.currentframe()
36
- caller = current_frame.f_back if current_frame else None
37
-
38
- if not caller:
39
- return False, None
55
+ plugin_frame = find_plugin_ancestor(inspect.currentframe())
40
56
 
41
- if "__is_plugin__" not in caller.f_globals:
42
- if depth > 0:
43
- return is_plugin_caller(frame=caller, depth=depth - 1)
44
- else:
45
- return False, None
57
+ if plugin_frame:
58
+ module = plugin_frame.f_globals.get("__name__")
59
+ qualname = plugin_frame.f_code.co_qualname
46
60
 
47
- module = caller.f_globals.get("__name__")
48
- qualname = caller.f_code.co_qualname
61
+ return True, f"{module}.{qualname}"
49
62
 
50
- return True, f"{module}.{qualname}"
63
+ return False, None
51
64
 
52
65
 
53
66
  __exports__ = ()
@@ -99,6 +99,13 @@ class Patient(Model):
99
99
  def __str__(self) -> str:
100
100
  return f"{self.first_name} {self.last_name}"
101
101
 
102
+ @property
103
+ def full_name(self) -> str:
104
+ """Returns the patient's full name."""
105
+ return " ".join(
106
+ n for n in (self.first_name, self.middle_name, self.last_name, self.suffix) if n
107
+ )
108
+
102
109
  def age_at(self, time: arrow.Arrow) -> float:
103
110
  """Given a datetime, returns what the patient's age would be at that datetime."""
104
111
  age = float(0)
@@ -212,6 +212,9 @@
212
212
  "canvas_sdk.effects.compound_medications.compound_medication": [
213
213
  "CompoundMedication"
214
214
  ],
215
+ "canvas_sdk.effects.group": [
216
+ "Group"
217
+ ],
215
218
  "canvas_sdk.effects.launch_modal": [
216
219
  "LaunchModalEffect"
217
220
  ],
@@ -236,6 +239,9 @@
236
239
  "canvas_sdk.effects.note.note": [
237
240
  "Note"
238
241
  ],
242
+ "canvas_sdk.effects.panel_configuration": [
243
+ "PanelConfiguration"
244
+ ],
239
245
  "canvas_sdk.effects.patient": [
240
246
  "CreatePatientExternalIdentifier",
241
247
  "Patient",
@@ -250,6 +256,9 @@
250
256
  "canvas_sdk.effects.patient.create_patient_external_identifier": [
251
257
  "CreatePatientExternalIdentifier"
252
258
  ],
259
+ "canvas_sdk.effects.patient_chart_group": [
260
+ "PatientChartGroup"
261
+ ],
253
262
  "canvas_sdk.effects.patient_chart_summary_configuration": [
254
263
  "PatientChartSummaryConfiguration"
255
264
  ],
@@ -279,6 +288,14 @@
279
288
  "canvas_sdk.effects.patient_profile_configuration": [
280
289
  "PatientProfileConfiguration"
281
290
  ],
291
+ "canvas_sdk.effects.payment_processor": [
292
+ "AddPaymentMethodResponse",
293
+ "CardTransaction",
294
+ "PaymentMethod",
295
+ "PaymentProcessorForm",
296
+ "PaymentProcessorMetadata",
297
+ "RemovePaymentMethodResponse"
298
+ ],
282
299
  "canvas_sdk.effects.protocol_card": [
283
300
  "ProtocolCard",
284
301
  "Recommendation"
@@ -364,6 +381,12 @@
364
381
  "canvas_sdk.handlers.cron_task": [
365
382
  "CronTask"
366
383
  ],
384
+ "canvas_sdk.handlers.payment_processors.base": [
385
+ "PaymentProcessor"
386
+ ],
387
+ "canvas_sdk.handlers.payment_processors.card": [
388
+ "CardPaymentProcessor"
389
+ ],
367
390
  "canvas_sdk.handlers.simple_api": [
368
391
  "APIKeyAuthMixin",
369
392
  "APIKeyCredentials",
@@ -147,6 +147,8 @@ def download_plugin(plugin_package: str) -> Generator[Path, None, None]:
147
147
 
148
148
  with open(download_path, "wb") as download_file:
149
149
  response = requests.request(method=method, url=f"https://{host}{path}", headers=headers)
150
+ response.raise_for_status()
151
+
150
152
  download_file.write(response.content)
151
153
 
152
154
  yield download_path
@@ -168,7 +170,8 @@ def install_plugin(plugin_name: str, attributes: PluginAttributes) -> None:
168
170
 
169
171
  install_plugin_secrets(plugin_name=plugin_name, secrets=attributes["secrets"])
170
172
  except Exception as e:
171
- log.error(f'Failed to install plugin "{plugin_name}", version {attributes["version"]}')
173
+ log.error(f'Failed to install plugin "{plugin_name}", version {attributes["version"]}: {e}')
174
+
172
175
  sentry_sdk.capture_exception(e)
173
176
 
174
177
  raise PluginInstallationError() from e
@@ -1,3 +1,4 @@
1
+ import base64
1
2
  import json
2
3
  import os
3
4
  import pathlib
@@ -213,6 +214,28 @@ class PluginRunner(PluginRunnerServicer):
213
214
  # respond to SimpleAPI request events are not relevant
214
215
  plugin_name = event.context["plugin_name"]
215
216
  relevant_plugins = [p for p in relevant_plugins if p.startswith(f"{plugin_name}:")]
217
+ elif event_type in {
218
+ EventType.REVENUE__PAYMENT_PROCESSOR__CHARGE,
219
+ EventType.REVENUE__PAYMENT_PROCESSOR__SELECTED,
220
+ EventType.REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHODS__LIST,
221
+ EventType.REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHODS__ADD,
222
+ EventType.REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHODS__REMOVE,
223
+ }:
224
+ # The target plugin's name will be part of the payment processor identifier, so other plugins that
225
+ # respond to payment processor charge events are not relevant
226
+ try:
227
+ plugin_name = (
228
+ base64.b64decode(event.context["identifier"]).decode("utf-8").split(".")[0]
229
+ )
230
+ relevant_plugins = [
231
+ p for p in relevant_plugins if p.startswith(f"{plugin_name}:")
232
+ ]
233
+ except Exception as ex:
234
+ log.error(
235
+ f"Failed to decode identifier for event {event_name} with context {event.context}"
236
+ )
237
+ sentry_sdk.capture_exception(ex)
238
+ relevant_plugins = []
216
239
 
217
240
  effect_list = []
218
241
 
plugin_runner/sandbox.py CHANGED
@@ -225,6 +225,7 @@ THIRD_PARTY_MODULES = {
225
225
  },
226
226
  "pydantic": {
227
227
  "BaseModel",
228
+ "ConfigDict",
228
229
  "conint",
229
230
  "constr",
230
231
  "Field",
@@ -303,6 +303,17 @@ enum EffectType {
303
303
 
304
304
  CREATE_COMPOUND_MEDICATION = 6020;
305
305
  UPDATE_COMPOUND_MEDICATION = 6021;
306
+
307
+ PATIENT_CHART__GROUP_ITEMS = 6030;
308
+
309
+ SHOW_PANEL_SECTIONS = 6100;
310
+
311
+ REVENUE__PAYMENT_PROCESSOR__METADATA = 7001;
312
+ REVENUE__PAYMENT_PROCESSOR__FORM = 7002;
313
+ REVENUE__PAYMENT_PROCESSOR__CREDIT_CARD_TRANSACTION = 7003;
314
+ REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHOD = 7004;
315
+ REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHOD__ADD_RESPONSE = 7005;
316
+ REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHOD__REMOVE_RESPONSE = 7006;
306
317
  }
307
318
 
308
319
  message Effect {
@@ -1108,6 +1108,7 @@ enum EventType {
1108
1108
  PATIENT_PROFILE__SECTION_CONFIGURATION = 100002;
1109
1109
  PATIENT_PROFILE__ADD_PHARMACY__POST_SEARCH = 100003;
1110
1110
 
1111
+ PATIENT_CHART__MEDICATIONS = 100004;
1111
1112
  CLAIM__CONDITIONS = 101000;
1112
1113
 
1113
1114
  PLUGIN_CREATED = 102000;
@@ -1160,6 +1161,15 @@ enum EventType {
1160
1161
  DOCUMENT_REFERENCE_CREATED = 150000;
1161
1162
  DOCUMENT_REFERENCE_UPDATED = 150001;
1162
1163
  DOCUMENT_REFERENCE_DELETED = 150002;
1164
+
1165
+ PANEL_SECTIONS_CONFIGURATION = 15100;
1166
+
1167
+ REVENUE__PAYMENT_PROCESSOR__LIST = 160001;
1168
+ REVENUE__PAYMENT_PROCESSOR__CHARGE = 160003;
1169
+ REVENUE__PAYMENT_PROCESSOR__SELECTED = 160002;
1170
+ REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHODS__LIST = 160004;
1171
+ REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHODS__ADD = 160005;
1172
+ REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHODS__REMOVE = 160006;
1163
1173
  }
1164
1174
 
1165
1175
  message Event {