canvas 0.34.1__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.1.dist-info → canvas-0.35.0.dist-info}/METADATA +1 -1
- {canvas-0.34.1.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 +3 -0
- 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.1.dist-info → canvas-0.35.0.dist-info}/WHEEL +0 -0
- {canvas-0.34.1.dist-info → canvas-0.35.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from collections.abc import Callable, Generator
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from datetime import timedelta
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from typing import Any, TypeVar, cast, overload
|
|
7
|
+
|
|
8
|
+
from django.conf import settings
|
|
9
|
+
from statsd.client.base import StatsClientBase
|
|
10
|
+
from statsd.client.udp import Pipeline
|
|
11
|
+
from statsd.defaults.env import statsd as default_statsd_client
|
|
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
|
+
def get_qualified_name(fn: Callable) -> str:
|
|
35
|
+
"""Get the qualified name of a function."""
|
|
36
|
+
return f"{fn.__module__}.{fn.__qualname__}"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class StatsDClientProxy:
|
|
40
|
+
"""Proxy for a StatsD client."""
|
|
41
|
+
|
|
42
|
+
def __init__(self) -> None:
|
|
43
|
+
self.client = default_statsd_client
|
|
44
|
+
|
|
45
|
+
def gauge(self, metric_name: str, value: float, tags: dict[str, str]) -> None:
|
|
46
|
+
"""Sends a gauge metric to StatsD with properly formatted tags.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
metric_name (str): The name of the metric.
|
|
50
|
+
value (float): The value to report.
|
|
51
|
+
tags (dict[str, str]): Dictionary of tags to attach to the metric.
|
|
52
|
+
"""
|
|
53
|
+
if not settings.METRICS_ENABLED:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
statsd_tags = tags_to_line_protocol(tags)
|
|
57
|
+
self.client.gauge(f"{metric_name},{statsd_tags}", value)
|
|
58
|
+
|
|
59
|
+
def timing(self, metric_name: str, delta: float | timedelta, tags: dict[str, str]) -> None:
|
|
60
|
+
"""Sends a timing metric to StatsD with properly formatted tags.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
metric_name (str): The name of the metric.
|
|
64
|
+
delta (float | timedelta): The value to report.
|
|
65
|
+
tags (dict[str, str]): Dictionary of tags to attach to the metric.
|
|
66
|
+
"""
|
|
67
|
+
if not settings.METRICS_ENABLED:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
statsd_tags = tags_to_line_protocol(tags)
|
|
71
|
+
self.client.timing(f"{metric_name},{statsd_tags}", delta)
|
|
72
|
+
|
|
73
|
+
def incr(self, metric_name: str, tags: dict[str, str], count: int = 1, rate: int = 1) -> None:
|
|
74
|
+
"""Sends an increment metric to StatsD with properly formatted tags.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
metric_name (str): The name of the metric.
|
|
78
|
+
count (int): The increment to report.
|
|
79
|
+
rate (int): The sample rate.
|
|
80
|
+
tags (dict[str, str]): Dictionary of tags to attach to the metric.
|
|
81
|
+
"""
|
|
82
|
+
if not settings.METRICS_ENABLED:
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
statsd_tags = tags_to_line_protocol(tags)
|
|
86
|
+
self.client.incr(f"{metric_name},{statsd_tags}", count, rate)
|
|
87
|
+
|
|
88
|
+
def pipeline(self) -> "PipelineProxy":
|
|
89
|
+
"""Returns a pipeline for batching StatsD metrics."""
|
|
90
|
+
return PipelineProxy(self.client)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class PipelineProxy(StatsDClientProxy):
|
|
94
|
+
"""Proxy for a StatsD pipeline."""
|
|
95
|
+
|
|
96
|
+
def __init__(self, client: StatsClientBase | None) -> None:
|
|
97
|
+
super().__init__()
|
|
98
|
+
self.client = Pipeline(client or self.client)
|
|
99
|
+
|
|
100
|
+
def send(self) -> None:
|
|
101
|
+
"""Sends the batched metrics to StatsD."""
|
|
102
|
+
if not settings.METRICS_ENABLED:
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
self.client.send()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
statsd_client = StatsDClientProxy()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@contextmanager
|
|
112
|
+
def measure(
|
|
113
|
+
name: str,
|
|
114
|
+
extra_tags: dict[str, str] | None = None,
|
|
115
|
+
client: StatsDClientProxy | None = None,
|
|
116
|
+
track_plugins_usage: bool = False,
|
|
117
|
+
) -> Generator[PipelineProxy, None, None]:
|
|
118
|
+
"""A context manager for collecting metrics about a context block.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
name: The name of the block being measured (added as a StatsD tag)
|
|
122
|
+
extra_tags: A dict of extra tags to be added to all recorded metrics.
|
|
123
|
+
client: An optional alternate StatsD client.
|
|
124
|
+
track_plugins_usage: Whether to track plugin usage (Adds plugin and handler tags if the caller was a plugin).
|
|
125
|
+
|
|
126
|
+
Yields:
|
|
127
|
+
A pipeline for collecting additional metrics in the same batch.
|
|
128
|
+
"""
|
|
129
|
+
client = client or statsd_client
|
|
130
|
+
|
|
131
|
+
if track_plugins_usage:
|
|
132
|
+
from canvas_sdk.utils.plugins import is_plugin_caller
|
|
133
|
+
|
|
134
|
+
is_plugin, caller = is_plugin_caller()
|
|
135
|
+
if is_plugin and caller:
|
|
136
|
+
extra_tags = extra_tags or {}
|
|
137
|
+
extra_tags["plugin"] = caller.split(".")[0]
|
|
138
|
+
extra_tags["handler"] = caller
|
|
139
|
+
|
|
140
|
+
tags = {"name": name, **(extra_tags or {})}
|
|
141
|
+
|
|
142
|
+
pipeline = client.pipeline()
|
|
143
|
+
timing_start = time.perf_counter_ns()
|
|
144
|
+
try:
|
|
145
|
+
yield pipeline
|
|
146
|
+
except BaseException as ex:
|
|
147
|
+
tags = {**tags, "status": "error"}
|
|
148
|
+
raise ex
|
|
149
|
+
else:
|
|
150
|
+
tags = {**tags, "status": "success"}
|
|
151
|
+
finally:
|
|
152
|
+
duration_ms = (time.perf_counter_ns() - timing_start) / 1_000_000
|
|
153
|
+
pipeline.timing("plugins.timings", duration_ms, tags=tags)
|
|
154
|
+
pipeline.incr("plugins.executions", tags=tags)
|
|
155
|
+
pipeline.send()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
F = TypeVar("F", bound=Callable)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@overload
|
|
162
|
+
def measured(fn: F) -> F: ...
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@overload
|
|
166
|
+
def measured(**options: Any) -> Callable[[F], F]: ...
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def measured(fn: F | None = None, **options: Any) -> Callable[[F], F] | F:
|
|
170
|
+
"""Collect metrics about the decorated function.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
fn: The decorated function.
|
|
174
|
+
options: Additional options for the decorator, such as `client` and `extra_tags`.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
A decorated function if called without arguments (@measured), or a
|
|
178
|
+
decorator if called with arguments (@measured(client=...))
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
def _decorator(fn: F) -> F:
|
|
182
|
+
@wraps(fn)
|
|
183
|
+
def _wrapped(*args: Any, **kwargs: Any) -> Any:
|
|
184
|
+
with measure(get_qualified_name(fn), **options):
|
|
185
|
+
return fn(*args, **kwargs)
|
|
186
|
+
|
|
187
|
+
return cast(F, _wrapped)
|
|
188
|
+
|
|
189
|
+
return _decorator(fn) if fn else _decorator
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
__exports__ = ()
|
canvas_sdk/utils/plugins.py
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
from collections.abc import Callable
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
from types import FrameType
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
7
|
+
from canvas_sdk.utils.metrics import measured
|
|
6
8
|
from settings import PLUGIN_DIRECTORY
|
|
7
9
|
|
|
8
10
|
|
|
11
|
+
@measured
|
|
9
12
|
def plugin_only(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
10
13
|
"""Decorator to restrict a function's execution to plugins only."""
|
|
11
14
|
|
|
@@ -26,4 +29,25 @@ def plugin_only(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
|
26
29
|
return wrapper
|
|
27
30
|
|
|
28
31
|
|
|
32
|
+
@measured
|
|
33
|
+
def is_plugin_caller(depth: int = 10, frame: FrameType | None = None) -> tuple[bool, str | None]:
|
|
34
|
+
"""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
|
|
40
|
+
|
|
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
|
|
46
|
+
|
|
47
|
+
module = caller.f_globals.get("__name__")
|
|
48
|
+
qualname = caller.f_code.co_qualname
|
|
49
|
+
|
|
50
|
+
return True, f"{module}.{qualname}"
|
|
51
|
+
|
|
52
|
+
|
|
29
53
|
__exports__ = ()
|
canvas_sdk/v1/data/__init__.py
CHANGED
|
@@ -21,6 +21,7 @@ from .lab import (
|
|
|
21
21
|
LabValueCoding,
|
|
22
22
|
)
|
|
23
23
|
from .medication import Medication, MedicationCoding
|
|
24
|
+
from .message import Message, MessageAttachment, MessageTransmission
|
|
24
25
|
from .note import Note, NoteType
|
|
25
26
|
from .observation import (
|
|
26
27
|
Observation,
|
|
@@ -89,6 +90,9 @@ __all__ = __exports__ = (
|
|
|
89
90
|
"LabValueCoding",
|
|
90
91
|
"Medication",
|
|
91
92
|
"MedicationCoding",
|
|
93
|
+
"Message",
|
|
94
|
+
"MessageAttachment",
|
|
95
|
+
"MessageTransmission",
|
|
92
96
|
"Note",
|
|
93
97
|
"NoteType",
|
|
94
98
|
"Observation",
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TransmissionChannel(models.TextChoices):
|
|
5
|
+
"""Transmission channel."""
|
|
6
|
+
|
|
7
|
+
MANUAL = "manual", "Manual"
|
|
8
|
+
TEXT_MESSAGE = "sms", "Text Message"
|
|
9
|
+
EMAIL = "email", "Email"
|
|
10
|
+
NOOP = "noop", "No-op"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Message(models.Model):
|
|
14
|
+
"""Message."""
|
|
15
|
+
|
|
16
|
+
class Meta:
|
|
17
|
+
managed = False
|
|
18
|
+
db_table = "canvas_sdk_data_api_message_001"
|
|
19
|
+
|
|
20
|
+
id = models.UUIDField()
|
|
21
|
+
dbid = models.BigIntegerField(primary_key=True)
|
|
22
|
+
created = models.DateTimeField()
|
|
23
|
+
modified = models.DateTimeField()
|
|
24
|
+
content = models.TextField()
|
|
25
|
+
sender = models.ForeignKey(
|
|
26
|
+
"v1.CanvasUser", on_delete=models.DO_NOTHING, related_name="sent_messages", null=True
|
|
27
|
+
)
|
|
28
|
+
recipient = models.ForeignKey(
|
|
29
|
+
"v1.CanvasUser", on_delete=models.DO_NOTHING, related_name="received_messages", null=True
|
|
30
|
+
)
|
|
31
|
+
note = models.ForeignKey(
|
|
32
|
+
"v1.Note", on_delete=models.DO_NOTHING, related_name="message", null=True
|
|
33
|
+
)
|
|
34
|
+
read = models.BooleanField()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class MessageAttachment(models.Model):
|
|
38
|
+
"""Message attachment."""
|
|
39
|
+
|
|
40
|
+
class Meta:
|
|
41
|
+
managed = False
|
|
42
|
+
db_table = "canvas_sdk_data_api_messageattachment_001"
|
|
43
|
+
|
|
44
|
+
id = models.UUIDField()
|
|
45
|
+
dbid = models.BigIntegerField(primary_key=True)
|
|
46
|
+
file = models.TextField()
|
|
47
|
+
content_type = models.CharField(max_length=255)
|
|
48
|
+
message = models.ForeignKey(
|
|
49
|
+
"v1.Message", on_delete=models.DO_NOTHING, related_name="message", null=True
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class MessageTransmission(models.Model):
|
|
54
|
+
"""Message Transmission."""
|
|
55
|
+
|
|
56
|
+
class Meta:
|
|
57
|
+
managed = False
|
|
58
|
+
db_table = "canvas_sdk_data_api_messagetransmission_001"
|
|
59
|
+
|
|
60
|
+
id = models.UUIDField()
|
|
61
|
+
dbid = models.BigIntegerField(primary_key=True)
|
|
62
|
+
created = models.DateTimeField()
|
|
63
|
+
modified = models.DateTimeField()
|
|
64
|
+
message = models.ForeignKey(
|
|
65
|
+
"v1.Message", on_delete=models.DO_NOTHING, related_name="transmissions", null=True
|
|
66
|
+
)
|
|
67
|
+
delivered = models.BooleanField()
|
|
68
|
+
failed = models.BooleanField()
|
|
69
|
+
|
|
70
|
+
contact_point_system = models.CharField(choices=TransmissionChannel.choices)
|
|
71
|
+
contact_point_value = models.CharField(max_length=255)
|
|
72
|
+
|
|
73
|
+
comment = models.TextField()
|
|
74
|
+
delivered_by = models.ForeignKey(
|
|
75
|
+
"v1.Staff",
|
|
76
|
+
on_delete=models.DO_NOTHING,
|
|
77
|
+
related_name="transmissions_delivered",
|
|
78
|
+
null=True,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
__exports__ = ("TransmissionChannel", "Message", "MessageAttachment", "MessageTransmission")
|
|
@@ -18,11 +18,8 @@ from canvas_generated.messages.events_pb2 import PLUGIN_CREATED
|
|
|
18
18
|
from canvas_generated.messages.events_pb2 import Event as EventRequest
|
|
19
19
|
from canvas_sdk.events.base import Event
|
|
20
20
|
from canvas_sdk.protocols.base import BaseProtocol
|
|
21
|
-
from canvas_sdk.utils import stats
|
|
22
21
|
from plugin_runner.plugin_runner import LOADED_PLUGINS, load_or_reload_plugin
|
|
23
22
|
|
|
24
|
-
stats.STATS_ENABLED = False
|
|
25
|
-
|
|
26
23
|
ORIGINAL_PATH = sys.path.copy()
|
|
27
24
|
|
|
28
25
|
|
plugin_runner/plugin_runner.py
CHANGED
|
@@ -5,7 +5,6 @@ import pathlib
|
|
|
5
5
|
import pickle
|
|
6
6
|
import pkgutil
|
|
7
7
|
import sys
|
|
8
|
-
import time
|
|
9
8
|
import traceback
|
|
10
9
|
from collections import defaultdict
|
|
11
10
|
from collections.abc import AsyncGenerator
|
|
@@ -29,7 +28,8 @@ from canvas_sdk.effects import Effect
|
|
|
29
28
|
from canvas_sdk.effects.simple_api import Response
|
|
30
29
|
from canvas_sdk.events import Event, EventRequest, EventResponse, EventType
|
|
31
30
|
from canvas_sdk.protocols import ClinicalQualityMeasure
|
|
32
|
-
from canvas_sdk.utils
|
|
31
|
+
from canvas_sdk.utils import metrics
|
|
32
|
+
from canvas_sdk.utils.metrics import measured, statsd_client
|
|
33
33
|
from logger import log
|
|
34
34
|
from plugin_runner.authentication import token_for_plugin
|
|
35
35
|
from plugin_runner.installation import install_plugins
|
|
@@ -177,121 +177,118 @@ class PluginRunner(PluginRunnerServicer):
|
|
|
177
177
|
self, request: EventRequest, context: Any
|
|
178
178
|
) -> AsyncGenerator[EventResponse, None]:
|
|
179
179
|
"""This is invoked when an event comes in."""
|
|
180
|
-
event_start_time = time.time()
|
|
181
180
|
event = Event(request)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
181
|
+
with metrics.measure(
|
|
182
|
+
metrics.get_qualified_name(self.HandleEvent), extra_tags={"event": event.name}
|
|
183
|
+
):
|
|
184
|
+
event_type = event.type
|
|
185
|
+
event_name = event.name
|
|
186
|
+
relevant_plugins = EVENT_HANDLER_MAP[event_name]
|
|
187
|
+
relevant_plugin_handlers = []
|
|
188
|
+
|
|
189
|
+
log.debug(f"Processing {relevant_plugins} for {event_name}")
|
|
190
|
+
sentry_sdk.set_tag("event-name", event_name)
|
|
191
|
+
|
|
192
|
+
if relevant_plugins:
|
|
193
|
+
await reconnect_if_needed()
|
|
194
|
+
|
|
195
|
+
if event_type in [EventType.PLUGIN_CREATED, EventType.PLUGIN_UPDATED]:
|
|
196
|
+
plugin_name = event.target.id
|
|
197
|
+
# filter only for the plugin(s) that were created/updated
|
|
198
|
+
relevant_plugins = [p for p in relevant_plugins if p.startswith(f"{plugin_name}:")]
|
|
199
|
+
elif event_type in {EventType.SIMPLE_API_AUTHENTICATE, EventType.SIMPLE_API_REQUEST}:
|
|
200
|
+
# The target plugin's name will be part of the home-app URL path, so other plugins that
|
|
201
|
+
# respond to SimpleAPI request events are not relevant
|
|
202
|
+
plugin_name = event.context["plugin_name"]
|
|
203
|
+
relevant_plugins = [p for p in relevant_plugins if p.startswith(f"{plugin_name}:")]
|
|
204
|
+
|
|
205
|
+
effect_list = []
|
|
206
|
+
|
|
207
|
+
for plugin_name in relevant_plugins:
|
|
208
|
+
log.debug(f"Processing {plugin_name}")
|
|
209
|
+
sentry_sdk.set_tag("plugin-name", plugin_name)
|
|
210
|
+
|
|
211
|
+
plugin = LOADED_PLUGINS[plugin_name]
|
|
212
|
+
handler_class = plugin["class"]
|
|
213
|
+
base_plugin_name = plugin_name.split(":")[0]
|
|
214
|
+
|
|
215
|
+
secrets = plugin.get("secrets", {})
|
|
216
|
+
|
|
217
|
+
secrets.update(
|
|
218
|
+
{"graphql_jwt": token_for_plugin(plugin_name=plugin_name, audience="home")}
|
|
219
|
+
)
|
|
212
220
|
|
|
213
|
-
|
|
221
|
+
try:
|
|
222
|
+
handler = handler_class(event, secrets, ENVIRONMENT)
|
|
214
223
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
224
|
+
if not handler.accept_event():
|
|
225
|
+
continue
|
|
226
|
+
relevant_plugin_handlers.append(handler_class)
|
|
218
227
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
228
|
+
classname = (
|
|
229
|
+
handler.__class__.__name__
|
|
230
|
+
if isinstance(handler, ClinicalQualityMeasure)
|
|
231
|
+
else None
|
|
232
|
+
)
|
|
233
|
+
handler_name = metrics.get_qualified_name(handler.compute)
|
|
234
|
+
with metrics.measure(
|
|
235
|
+
name=handler_name,
|
|
236
|
+
extra_tags={
|
|
237
|
+
"plugin": base_plugin_name,
|
|
238
|
+
"event": event_name,
|
|
239
|
+
},
|
|
240
|
+
):
|
|
241
|
+
_effects = await sync_to_async(handler.compute)()
|
|
242
|
+
effects = [
|
|
243
|
+
Effect(
|
|
244
|
+
type=effect.type,
|
|
245
|
+
payload=effect.payload,
|
|
246
|
+
plugin_name=base_plugin_name,
|
|
247
|
+
classname=classname,
|
|
248
|
+
handler_name=handler_name,
|
|
249
|
+
)
|
|
250
|
+
for effect in _effects
|
|
251
|
+
]
|
|
252
|
+
|
|
253
|
+
effects = validate_effects(effects)
|
|
254
|
+
|
|
255
|
+
apply_effects_to_context(effects, event=event)
|
|
256
|
+
|
|
257
|
+
log.info(f"{plugin_name}.compute() completed.")
|
|
258
|
+
|
|
259
|
+
except Exception as e:
|
|
260
|
+
log.error(f"Encountered exception in plugin {plugin_name}:")
|
|
261
|
+
|
|
262
|
+
for error_line_with_newlines in traceback.format_exception(e):
|
|
263
|
+
for error_line in error_line_with_newlines.split("\n"):
|
|
264
|
+
log.error(error_line)
|
|
265
|
+
|
|
266
|
+
sentry_sdk.capture_exception(e)
|
|
223
267
|
continue
|
|
224
|
-
relevant_plugin_handlers.append(handler_class)
|
|
225
268
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
269
|
+
effect_list += effects
|
|
270
|
+
|
|
271
|
+
sentry_sdk.set_tag("plugin-name", None)
|
|
272
|
+
|
|
273
|
+
# Special handling for SimpleAPI requests: if there were no relevant handlers (as determined
|
|
274
|
+
# by calling ignore_event on handlers), then set the effects list to be a single 404 Not
|
|
275
|
+
# Found response effect. If multiple handlers were able to respond, log an error and set the
|
|
276
|
+
# effects list to be a single 500 Internal Server Error response effect.
|
|
277
|
+
if event.type in {EventType.SIMPLE_API_AUTHENTICATE, EventType.SIMPLE_API_REQUEST}:
|
|
278
|
+
if len(relevant_plugin_handlers) == 0:
|
|
279
|
+
effect_list = [Response(status_code=HTTPStatus.NOT_FOUND).apply()]
|
|
280
|
+
elif len(relevant_plugin_handlers) > 1:
|
|
281
|
+
log.error(
|
|
282
|
+
f"Multiple handlers responded to {EventType.Name(EventType.SIMPLE_API_REQUEST)}"
|
|
283
|
+
f" {event.context['path']}"
|
|
240
284
|
)
|
|
241
|
-
|
|
242
|
-
]
|
|
243
|
-
|
|
244
|
-
effects = validate_effects(effects)
|
|
245
|
-
|
|
246
|
-
apply_effects_to_context(effects, event=event)
|
|
285
|
+
effect_list = [Response(status_code=HTTPStatus.INTERNAL_SERVER_ERROR).apply()]
|
|
247
286
|
|
|
248
|
-
|
|
287
|
+
# Don't log anything if a plugin handler didn't actually run.
|
|
288
|
+
if relevant_plugins:
|
|
289
|
+
log.info(f"Responded to Event {event_name}.")
|
|
249
290
|
|
|
250
|
-
|
|
251
|
-
statsd_client.timing(
|
|
252
|
-
"plugins.protocol_duration_ms",
|
|
253
|
-
delta=compute_duration,
|
|
254
|
-
tags={"plugin": plugin_name},
|
|
255
|
-
)
|
|
256
|
-
except Exception as e:
|
|
257
|
-
log.error(f"Encountered exception in plugin {plugin_name}:")
|
|
258
|
-
|
|
259
|
-
for error_line_with_newlines in traceback.format_exception(e):
|
|
260
|
-
for error_line in error_line_with_newlines.split("\n"):
|
|
261
|
-
log.error(error_line)
|
|
262
|
-
|
|
263
|
-
sentry_sdk.capture_exception(e)
|
|
264
|
-
|
|
265
|
-
continue
|
|
266
|
-
|
|
267
|
-
effect_list += effects
|
|
268
|
-
|
|
269
|
-
sentry_sdk.set_tag("plugin-name", None)
|
|
270
|
-
|
|
271
|
-
# Special handling for SimpleAPI requests: if there were no relevant handlers (as determined
|
|
272
|
-
# by calling ignore_event on handlers), then set the effects list to be a single 404 Not
|
|
273
|
-
# Found response effect. If multiple handlers were able to respond, log an error and set the
|
|
274
|
-
# effects list to be a single 500 Internal Server Error response effect.
|
|
275
|
-
if event.type in {EventType.SIMPLE_API_AUTHENTICATE, EventType.SIMPLE_API_REQUEST}:
|
|
276
|
-
if len(relevant_plugin_handlers) == 0:
|
|
277
|
-
effect_list = [Response(status_code=HTTPStatus.NOT_FOUND).apply()]
|
|
278
|
-
elif len(relevant_plugin_handlers) > 1:
|
|
279
|
-
log.error(
|
|
280
|
-
f"Multiple handlers responded to {EventType.Name(EventType.SIMPLE_API_REQUEST)}"
|
|
281
|
-
f" {event.context['path']}"
|
|
282
|
-
)
|
|
283
|
-
effect_list = [Response(status_code=HTTPStatus.INTERNAL_SERVER_ERROR).apply()]
|
|
284
|
-
|
|
285
|
-
event_duration = get_duration_ms(event_start_time)
|
|
286
|
-
|
|
287
|
-
# Don't log anything if a plugin handler didn't actually run.
|
|
288
|
-
if relevant_plugins:
|
|
289
|
-
log.info(f"Responded to Event {event_name} ({event_duration} ms)")
|
|
290
|
-
statsd_client.timing(
|
|
291
|
-
"plugins.event_duration_ms", delta=event_duration, tags={"event": event_name}
|
|
292
|
-
)
|
|
293
|
-
|
|
294
|
-
yield EventResponse(success=True, effects=effect_list)
|
|
291
|
+
yield EventResponse(success=True, effects=effect_list)
|
|
295
292
|
|
|
296
293
|
async def ReloadPlugins(
|
|
297
294
|
self, request: ReloadPluginsRequest, context: Any
|
|
@@ -560,6 +557,7 @@ def refresh_event_type_map() -> None:
|
|
|
560
557
|
log.warning(f"Unknown RESPONDS_TO type: {type(responds_to)}")
|
|
561
558
|
|
|
562
559
|
|
|
560
|
+
@measured
|
|
563
561
|
def load_plugins(specified_plugin_paths: list[str] | None = None) -> None:
|
|
564
562
|
"""Load the plugins."""
|
|
565
563
|
# first mark each plugin as inactive since we want to remove it from
|
|
@@ -596,14 +594,9 @@ def load_plugins(specified_plugin_paths: list[str] | None = None) -> None:
|
|
|
596
594
|
|
|
597
595
|
refresh_event_type_map()
|
|
598
596
|
|
|
599
|
-
log_nr_event_handlers()
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
def log_nr_event_handlers() -> None:
|
|
603
|
-
"""Log the number of event handlers for each event."""
|
|
604
597
|
for key in EventType.keys(): # noqa: SIM118
|
|
605
598
|
value = len(EVENT_HANDLER_MAP[key]) if key in EVENT_HANDLER_MAP else 0
|
|
606
|
-
statsd_client.
|
|
599
|
+
statsd_client.gauge("plugins.event_handler_count", value, tags={"event": key})
|
|
607
600
|
|
|
608
601
|
|
|
609
602
|
_cleanup_coroutines = []
|
plugin_runner/sandbox.py
CHANGED
|
@@ -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;
|