canvas 0.34.0__py3-none-any.whl → 0.35.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 (59) hide show
  1. {canvas-0.34.0.dist-info → canvas-0.35.0.dist-info}/METADATA +1 -1
  2. {canvas-0.34.0.dist-info → canvas-0.35.0.dist-info}/RECORD +58 -50
  3. canvas_generated/messages/effects_pb2.py +4 -4
  4. canvas_generated/messages/effects_pb2.pyi +22 -2
  5. canvas_generated/messages/events_pb2.py +2 -2
  6. canvas_generated/messages/events_pb2.pyi +30 -0
  7. canvas_sdk/base.py +56 -0
  8. canvas_sdk/commands/base.py +22 -46
  9. canvas_sdk/commands/commands/adjust_prescription.py +0 -10
  10. canvas_sdk/commands/commands/allergy.py +0 -1
  11. canvas_sdk/commands/commands/assess.py +2 -2
  12. canvas_sdk/commands/commands/change_medication.py +58 -0
  13. canvas_sdk/commands/commands/close_goal.py +0 -1
  14. canvas_sdk/commands/commands/diagnose.py +0 -1
  15. canvas_sdk/commands/commands/exam.py +0 -1
  16. canvas_sdk/commands/commands/family_history.py +0 -1
  17. canvas_sdk/commands/commands/follow_up.py +4 -2
  18. canvas_sdk/commands/commands/goal.py +8 -7
  19. canvas_sdk/commands/commands/history_present_illness.py +0 -1
  20. canvas_sdk/commands/commands/imaging_order.py +9 -8
  21. canvas_sdk/commands/commands/instruct.py +2 -2
  22. canvas_sdk/commands/commands/lab_order.py +10 -9
  23. canvas_sdk/commands/commands/medical_history.py +0 -1
  24. canvas_sdk/commands/commands/medication_statement.py +0 -1
  25. canvas_sdk/commands/commands/past_surgical_history.py +0 -1
  26. canvas_sdk/commands/commands/perform.py +3 -2
  27. canvas_sdk/commands/commands/plan.py +0 -1
  28. canvas_sdk/commands/commands/prescribe.py +0 -9
  29. canvas_sdk/commands/commands/refer.py +10 -10
  30. canvas_sdk/commands/commands/refill.py +0 -9
  31. canvas_sdk/commands/commands/remove_allergy.py +0 -1
  32. canvas_sdk/commands/commands/resolve_condition.py +3 -2
  33. canvas_sdk/commands/commands/review_of_systems.py +0 -1
  34. canvas_sdk/commands/commands/stop_medication.py +0 -1
  35. canvas_sdk/commands/commands/structured_assessment.py +0 -1
  36. canvas_sdk/commands/commands/task.py +0 -4
  37. canvas_sdk/commands/commands/update_diagnosis.py +8 -6
  38. canvas_sdk/commands/commands/update_goal.py +0 -1
  39. canvas_sdk/commands/commands/vitals.py +0 -1
  40. canvas_sdk/effects/note/__init__.py +10 -0
  41. canvas_sdk/effects/note/appointment.py +148 -0
  42. canvas_sdk/effects/note/base.py +129 -0
  43. canvas_sdk/effects/note/note.py +79 -0
  44. canvas_sdk/effects/patient/__init__.py +3 -0
  45. canvas_sdk/effects/patient/base.py +123 -0
  46. canvas_sdk/utils/http.py +7 -26
  47. canvas_sdk/utils/metrics.py +192 -0
  48. canvas_sdk/utils/plugins.py +24 -0
  49. canvas_sdk/v1/data/__init__.py +4 -0
  50. canvas_sdk/v1/data/message.py +82 -0
  51. plugin_runner/load_all_plugins.py +0 -3
  52. plugin_runner/plugin_runner.py +107 -114
  53. plugin_runner/sandbox.py +102 -8
  54. protobufs/canvas_generated/messages/effects.proto +13 -0
  55. protobufs/canvas_generated/messages/events.proto +16 -0
  56. settings.py +4 -0
  57. canvas_sdk/utils/stats.py +0 -74
  58. {canvas-0.34.0.dist-info → canvas-0.35.0.dist-info}/WHEEL +0 -0
  59. {canvas-0.34.0.dist-info → canvas-0.35.0.dist-info}/entry_points.txt +0 -0
plugin_runner/sandbox.py CHANGED
@@ -206,6 +206,9 @@ STANDARD_LIBRARY_MODULES = {
206
206
  "Type",
207
207
  "TypedDict",
208
208
  },
209
+ "urllib": {
210
+ "parse",
211
+ },
209
212
  "urllib.parse": {
210
213
  "urlencode",
211
214
  "quote",
@@ -214,6 +217,9 @@ STANDARD_LIBRARY_MODULES = {
214
217
  "uuid4",
215
218
  "UUID",
216
219
  },
220
+ "zoneinfo": {
221
+ "ZoneInfo",
222
+ },
217
223
  }
218
224
 
219
225
 
@@ -309,6 +315,22 @@ def _find_folder_in_path(file_path: Path, target_folder_name: str) -> Path | Non
309
315
  return _find_folder_in_path(file_path.parent, target_folder_name)
310
316
 
311
317
 
318
+ def node_name(node: ast.AST) -> str:
319
+ """
320
+ Given an AST node, return its name.
321
+ """
322
+ if isinstance(node, ast.Call):
323
+ return ".".join(node_name(arg) for arg in node.args)
324
+
325
+ if isinstance(node, ast.Constant):
326
+ return str(node.value)
327
+
328
+ if isinstance(node, ast.Name):
329
+ return str(node.id)
330
+
331
+ return "__unknown__"
332
+
333
+
312
334
  class Sandbox:
313
335
  """A restricted sandbox for safely executing arbitrary Python code."""
314
336
 
@@ -483,7 +505,7 @@ class Sandbox:
483
505
  func=ast.Name("_write_", ast.Load()),
484
506
  args=[
485
507
  node.value,
486
- ast.Constant(node.value.id if isinstance(node.value, ast.Name) else None),
508
+ ast.Constant(node_name(node.value)),
487
509
  ast.Constant(node.attr),
488
510
  ],
489
511
  keywords=[],
@@ -497,6 +519,55 @@ class Sandbox:
497
519
  # Impossible Case only ctx Load, Store and Del are defined in ast.
498
520
  raise NotImplementedError(f"Unknown ctx type: {type(node.ctx)}")
499
521
 
522
+ def visit_Subscript(self, node: ast.Subscript) -> ast.AST:
523
+ """Transforms all kinds of subscripts.
524
+
525
+ 'foo[bar]' becomes '_getitem_(foo, bar)'
526
+ 'foo[:ab]' becomes '_getitem_(foo, slice(None, ab, None))'
527
+ 'foo[ab:]' becomes '_getitem_(foo, slice(ab, None, None))'
528
+ 'foo[a:b]' becomes '_getitem_(foo, slice(a, b, None))'
529
+ 'foo[a:b:c]' becomes '_getitem_(foo, slice(a, b, c))'
530
+ 'foo[a, b:c] becomes '_getitem_(foo, (a, slice(b, c, None)))'
531
+ 'foo[a] = c' becomes '_write_(foo)[a] = c'
532
+ 'del foo[a]' becomes 'del _write_(foo)[a]'
533
+
534
+ The _write_ function should return a security proxy.
535
+ """
536
+ node = self.node_contents_visit(node)
537
+
538
+ # 'AugStore' and 'AugLoad' are defined in 'Python.asdl' as possible
539
+ # 'expr_context'. However, according to Python/ast.c
540
+ # they are NOT used by the implementation => No need to worry here.
541
+ # Instead ast.c creates 'AugAssign' nodes, which can be visited.
542
+ if isinstance(node.ctx, ast.Load):
543
+ new_node = ast.Call(
544
+ func=ast.Name("_getitem_", ast.Load()),
545
+ args=[node.value, self.transform_slice(node.slice)],
546
+ keywords=[],
547
+ )
548
+
549
+ copy_locations(new_node, node)
550
+
551
+ return new_node
552
+ elif isinstance(node.ctx, ast.Del | ast.Store):
553
+ new_value = ast.Call(
554
+ func=ast.Name("_write_", ast.Load()),
555
+ args=[
556
+ node.value,
557
+ ast.Constant(node_name(node.value)),
558
+ ast.Constant(node_name(node.slice)),
559
+ ],
560
+ keywords=[],
561
+ )
562
+
563
+ copy_locations(new_value, node)
564
+ node.value = new_value
565
+
566
+ return node
567
+ else: # pragma: no cover
568
+ # Impossible Case only ctx Load, Store and Del are defined in ast.
569
+ raise NotImplementedError(f"Unknown ctx type: {type(node.ctx)}")
570
+
500
571
  def __init__(
501
572
  self,
502
573
  source_code: Path,
@@ -655,7 +726,12 @@ class Sandbox:
655
726
  """
656
727
  return bool(self.base_path) and module.split(".")[0] == self.package_name
657
728
 
658
- def _safe_write(self, _ob: Any, name: str | None = None, attribute: str | None = None) -> Any:
729
+ def _safe_write(
730
+ self,
731
+ _ob: Any,
732
+ name: str | None = None,
733
+ attribute: str | int | None = None,
734
+ ) -> Any:
659
735
  """Check if the given obj belongs to a protected resource."""
660
736
  is_module = isinstance(_ob, types.ModuleType)
661
737
 
@@ -664,14 +740,30 @@ class Sandbox:
664
740
  raise AttributeError(f"Forbidden assignment to a module attribute: {_ob.__name__}.")
665
741
  elif isinstance(_ob, type):
666
742
  full_name = f"{_ob.__module__}.{_ob.__qualname__}"
743
+ module_name = _ob.__module__
744
+ else:
745
+ full_name = f"{_ob.__class__.__module__}.{_ob.__class__.__qualname__}"
746
+ module_name = _ob.__class__.__module__
747
+
748
+ if attribute is not None:
749
+ if isinstance(_ob, dict):
750
+ value = dict.get(_ob, attribute)
751
+ elif isinstance(_ob, list | tuple) and isinstance(attribute, int):
752
+ value = _ob.__getitem__(attribute)
753
+ elif isinstance(attribute, str):
754
+ value = getattr(_ob, attribute, None)
755
+ else:
756
+ value = None
667
757
  else:
668
- full_name = f"{_ob.__module__}.{_ob.__class__.__qualname__}"
758
+ value = None
669
759
 
670
- if not self._same_module(_ob.__module__) and (
760
+ if not self._same_module(module_name) and (
671
761
  # deny if it was anything imported
672
- name in self.imported_names["names"]
762
+ (name and name.split(".")[0] in self.imported_names["names"])
673
763
  # deny if it's anything callable
674
- or (attribute is not None and callable(getattr(_ob, attribute)))
764
+ or callable(value)
765
+ # deny writes to dictionary underscore keys
766
+ or (isinstance(_ob, dict) and isinstance(attribute, str) and attribute.startswith("_"))
675
767
  ):
676
768
  raise AttributeError(
677
769
  f"Forbidden assignment to a non-module attribute: {full_name} "
@@ -704,7 +796,7 @@ class Sandbox:
704
796
  is_module = isinstance(_ob, types.ModuleType)
705
797
 
706
798
  if is_module:
707
- module = _ob.__name__.split(".")[0]
799
+ module = _ob.__name__
708
800
  elif isinstance(_ob, type):
709
801
  module = _ob.__module__.split(".")[0]
710
802
  else:
@@ -743,7 +835,9 @@ class Sandbox:
743
835
  if name not in exports:
744
836
  raise AttributeError(f'"{name}" is an invalid attribute name (not in __exports__)')
745
837
  elif is_module and (module not in ALLOWED_MODULES or name not in ALLOWED_MODULES[module]):
746
- raise AttributeError(f'"{name}" is an invalid attribute name (not in ALLOWED_MODULES)')
838
+ raise AttributeError(
839
+ f'"{module}.{name}" is an invalid attribute name (not in ALLOWED_MODULES)'
840
+ )
747
841
 
748
842
  return getattr(_ob, name, default)
749
843
 
@@ -169,6 +169,12 @@ enum EffectType {
169
169
  COMMIT_REFER_COMMAND = 142;
170
170
  ENTER_IN_ERROR_REFER_COMMAND = 143;
171
171
 
172
+ ORIGINATE_CHANGE_MEDICATION_COMMAND = 150;
173
+ EDIT_CHANGE_MEDICATION_COMMAND = 151;
174
+ DELETE_CHANGE_MEDICATION_COMMAND = 152;
175
+ COMMIT_CHANGE_MEDICATION_COMMAND = 153;
176
+ ENTER_IN_ERROR_CHANGE_MEDICATION_COMMAND = 154;
177
+
172
178
  CREATE_QUESTIONNAIRE_RESULT = 138;
173
179
 
174
180
  ANNOTATE_PATIENT_CHART_CONDITION_RESULTS = 200;
@@ -256,6 +262,12 @@ enum EffectType {
256
262
  SIMPLE_API_RESPONSE = 4000;
257
263
 
258
264
  UPDATE_USER = 5000;
265
+
266
+ CREATE_NOTE = 6000;
267
+ CREATE_APPOINTMENT = 6001;
268
+ CREATE_SCHEDULE_EVENT = 6002;
269
+
270
+ CREATE_PATIENT = 6003;
259
271
  }
260
272
 
261
273
  message Effect {
@@ -263,6 +275,7 @@ message Effect {
263
275
  string payload = 2;
264
276
  string plugin_name = 3;
265
277
  string classname = 4;
278
+ string handler_name = 5;
266
279
  //Oneof effect_payload {
267
280
  // ...
268
281
  //}
@@ -1028,6 +1028,22 @@ enum EventType {
1028
1028
  DEFER_CODING_GAP_COMMAND__POST_EXECUTE_ACTION = 60011;
1029
1029
  DEFER_CODING_GAP_COMMAND__POST_INSERTED_INTO_NOTE = 60012;
1030
1030
 
1031
+ CHANGE_MEDICATION_COMMAND__PRE_ORIGINATE = 61000;
1032
+ CHANGE_MEDICATION_COMMAND__POST_ORIGINATE = 61001;
1033
+ CHANGE_MEDICATION_COMMAND__PRE_UPDATE = 61002;
1034
+ CHANGE_MEDICATION_COMMAND__POST_UPDATE = 61003;
1035
+ CHANGE_MEDICATION_COMMAND__PRE_COMMIT = 61004;
1036
+ CHANGE_MEDICATION_COMMAND__POST_COMMIT = 61005;
1037
+ CHANGE_MEDICATION_COMMAND__PRE_DELETE = 61006;
1038
+ CHANGE_MEDICATION_COMMAND__POST_DELETE = 61007;
1039
+ CHANGE_MEDICATION_COMMAND__PRE_ENTER_IN_ERROR = 61008;
1040
+ CHANGE_MEDICATION_COMMAND__POST_ENTER_IN_ERROR = 61009;
1041
+ CHANGE_MEDICATION_COMMAND__PRE_EXECUTE_ACTION = 61010;
1042
+ CHANGE_MEDICATION_COMMAND__POST_EXECUTE_ACTION = 61011;
1043
+ CHANGE_MEDICATION_COMMAND__POST_INSERTED_INTO_NOTE = 61012;
1044
+ CHANGE_MEDICATION__MEDICATION__PRE_SEARCH = 61013;
1045
+ CHANGE_MEDICATION__MEDICATION__POST_SEARCH = 61014;
1046
+
1031
1047
  SHOW_NOTE_HEADER_BUTTON = 70000;
1032
1048
  SHOW_NOTE_FOOTER_BUTTON = 70001;
1033
1049
  ACTION_BUTTON_CLICKED = 70002;
settings.py CHANGED
@@ -14,6 +14,7 @@ ENV = os.getenv("ENV", "development")
14
14
  IS_PRODUCTION = ENV == "production"
15
15
  IS_PRODUCTION_CUSTOMER = env_to_bool("IS_PRODUCTION_CUSTOMER", IS_PRODUCTION)
16
16
  IS_TESTING = env_to_bool("IS_TESTING", "pytest" in sys.argv[0] or sys.argv[0] == "-c")
17
+ IS_SCRIPT = env_to_bool("IS_SCRIPT", "plugin_runner.py" not in sys.argv[0])
17
18
  CUSTOMER_IDENTIFIER = os.getenv("CUSTOMER_IDENTIFIER", "local")
18
19
  APP_NAME = os.getenv("APP_NAME")
19
20
 
@@ -24,6 +25,9 @@ INTEGRATION_TEST_CLIENT_SECRET = os.getenv("INTEGRATION_TEST_CLIENT_SECRET")
24
25
  GRAPHQL_ENDPOINT = os.getenv("GRAPHQL_ENDPOINT", "http://localhost:8000/plugins-graphql")
25
26
  REDIS_ENDPOINT = os.getenv("REDIS_ENDPOINT", f"redis://{APP_NAME}-redis:6379")
26
27
 
28
+
29
+ METRICS_ENABLED = env_to_bool("PLUGINS_METRICS_ENABLED", not IS_SCRIPT)
30
+
27
31
  INSTALLED_APPS = [
28
32
  "canvas_sdk.v1",
29
33
  ]
canvas_sdk/utils/stats.py DELETED
@@ -1,74 +0,0 @@
1
- from datetime import timedelta
2
- from time import time
3
- from typing import Any
4
-
5
- from statsd.defaults.env import statsd as default_statsd_client
6
-
7
-
8
- def get_duration_ms(start_time: float) -> int:
9
- """Get the duration in milliseconds since the given start time."""
10
- return int((time() - start_time) * 1000)
11
-
12
-
13
- LINE_PROTOCOL_TRANSLATION = str.maketrans(
14
- {
15
- ",": r"\,",
16
- "=": r"\=",
17
- " ": r"\ ",
18
- ":": r"__",
19
- }
20
- )
21
-
22
-
23
- def tags_to_line_protocol(tags: dict[str, Any]) -> str:
24
- """Generate a tags string compatible with the InfluxDB line protocol.
25
-
26
- See: https://docs.influxdata.com/influxdb/v1.1/write_protocols/line_protocol_tutorial/
27
- """
28
- return ",".join(
29
- f"{tag_name}={str(tag_value).translate(LINE_PROTOCOL_TRANSLATION)}"
30
- for tag_name, tag_value in tags.items()
31
- )
32
-
33
-
34
- STATS_ENABLED = True
35
-
36
-
37
- class StatsDClientProxy:
38
- """Proxy for a StatsD client."""
39
-
40
- def __init__(self) -> None:
41
- self.client = default_statsd_client
42
-
43
- def gauge(self, metric_name: str, value: float, tags: dict[str, str]) -> None:
44
- """Sends a gauge metric to StatsD with properly formatted tags.
45
-
46
- Args:
47
- metric_name (str): The name of the metric.
48
- value (float): The value to report.
49
- tags (dict[str, str]): Dictionary of tags to attach to the metric.
50
- """
51
- if not STATS_ENABLED:
52
- return
53
-
54
- statsd_tags = tags_to_line_protocol(tags)
55
- self.client.gauge(f"{metric_name},{statsd_tags}", value)
56
-
57
- def timing(self, metric_name: str, delta: float | timedelta, tags: dict[str, str]) -> None:
58
- """Sends a timing metric to StatsD with properly formatted tags.
59
-
60
- Args:
61
- metric_name (str): The name of the metric.
62
- delta (float | timedelta): The value to report.
63
- tags (dict[str, str]): Dictionary of tags to attach to the metric.
64
- """
65
- if not STATS_ENABLED:
66
- return
67
-
68
- statsd_tags = tags_to_line_protocol(tags)
69
- self.client.timing(f"{metric_name},{statsd_tags}", delta)
70
-
71
-
72
- statsd_client = StatsDClientProxy()
73
-
74
- __exports__ = ()