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.
- {canvas-0.34.0.dist-info → canvas-0.35.0.dist-info}/METADATA +1 -1
- {canvas-0.34.0.dist-info → canvas-0.35.0.dist-info}/RECORD +58 -50
- canvas_generated/messages/effects_pb2.py +4 -4
- canvas_generated/messages/effects_pb2.pyi +22 -2
- canvas_generated/messages/events_pb2.py +2 -2
- canvas_generated/messages/events_pb2.pyi +30 -0
- canvas_sdk/base.py +56 -0
- canvas_sdk/commands/base.py +22 -46
- canvas_sdk/commands/commands/adjust_prescription.py +0 -10
- canvas_sdk/commands/commands/allergy.py +0 -1
- canvas_sdk/commands/commands/assess.py +2 -2
- canvas_sdk/commands/commands/change_medication.py +58 -0
- canvas_sdk/commands/commands/close_goal.py +0 -1
- canvas_sdk/commands/commands/diagnose.py +0 -1
- canvas_sdk/commands/commands/exam.py +0 -1
- canvas_sdk/commands/commands/family_history.py +0 -1
- canvas_sdk/commands/commands/follow_up.py +4 -2
- canvas_sdk/commands/commands/goal.py +8 -7
- canvas_sdk/commands/commands/history_present_illness.py +0 -1
- canvas_sdk/commands/commands/imaging_order.py +9 -8
- canvas_sdk/commands/commands/instruct.py +2 -2
- canvas_sdk/commands/commands/lab_order.py +10 -9
- canvas_sdk/commands/commands/medical_history.py +0 -1
- canvas_sdk/commands/commands/medication_statement.py +0 -1
- canvas_sdk/commands/commands/past_surgical_history.py +0 -1
- canvas_sdk/commands/commands/perform.py +3 -2
- canvas_sdk/commands/commands/plan.py +0 -1
- canvas_sdk/commands/commands/prescribe.py +0 -9
- canvas_sdk/commands/commands/refer.py +10 -10
- canvas_sdk/commands/commands/refill.py +0 -9
- canvas_sdk/commands/commands/remove_allergy.py +0 -1
- canvas_sdk/commands/commands/resolve_condition.py +3 -2
- canvas_sdk/commands/commands/review_of_systems.py +0 -1
- canvas_sdk/commands/commands/stop_medication.py +0 -1
- canvas_sdk/commands/commands/structured_assessment.py +0 -1
- canvas_sdk/commands/commands/task.py +0 -4
- canvas_sdk/commands/commands/update_diagnosis.py +8 -6
- canvas_sdk/commands/commands/update_goal.py +0 -1
- canvas_sdk/commands/commands/vitals.py +0 -1
- canvas_sdk/effects/note/__init__.py +10 -0
- canvas_sdk/effects/note/appointment.py +148 -0
- canvas_sdk/effects/note/base.py +129 -0
- canvas_sdk/effects/note/note.py +79 -0
- canvas_sdk/effects/patient/__init__.py +3 -0
- canvas_sdk/effects/patient/base.py +123 -0
- canvas_sdk/utils/http.py +7 -26
- canvas_sdk/utils/metrics.py +192 -0
- canvas_sdk/utils/plugins.py +24 -0
- canvas_sdk/v1/data/__init__.py +4 -0
- canvas_sdk/v1/data/message.py +82 -0
- plugin_runner/load_all_plugins.py +0 -3
- plugin_runner/plugin_runner.py +107 -114
- plugin_runner/sandbox.py +102 -8
- protobufs/canvas_generated/messages/effects.proto +13 -0
- protobufs/canvas_generated/messages/events.proto +16 -0
- settings.py +4 -0
- canvas_sdk/utils/stats.py +0 -74
- {canvas-0.34.0.dist-info → canvas-0.35.0.dist-info}/WHEEL +0 -0
- {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(
|
|
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(
|
|
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
|
-
|
|
758
|
+
value = None
|
|
669
759
|
|
|
670
|
-
if not self._same_module(
|
|
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
|
|
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__
|
|
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(
|
|
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__ = ()
|
|
File without changes
|
|
File without changes
|