canvas 0.34.1__py3-none-any.whl → 0.35.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.

Files changed (60) hide show
  1. {canvas-0.34.1.dist-info → canvas-0.35.1.dist-info}/METADATA +2 -2
  2. {canvas-0.34.1.dist-info → canvas-0.35.1.dist-info}/RECORD +59 -51
  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 +159 -198
  53. plugin_runner/sandbox.py +3 -0
  54. protobufs/canvas_generated/messages/effects.proto +13 -0
  55. protobufs/canvas_generated/messages/events.proto +16 -0
  56. pubsub/pubsub.py +10 -1
  57. settings.py +8 -0
  58. canvas_sdk/utils/stats.py +0 -74
  59. {canvas-0.34.1.dist-info → canvas-0.35.1.dist-info}/WHEEL +0 -0
  60. {canvas-0.34.1.dist-info → canvas-0.35.1.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__ = ()
@@ -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__ = ()
@@ -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