canvas 0.16.0__py3-none-any.whl → 0.18.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.

@@ -136,6 +136,20 @@ class EventType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
136
136
  ASSESS_COMMAND__POST_EXECUTE_ACTION: _ClassVar[EventType]
137
137
  ASSESS__CONDITION__POST_SEARCH: _ClassVar[EventType]
138
138
  ASSESS__CONDITION__PRE_SEARCH: _ClassVar[EventType]
139
+ CANCEL_PRESCRIPTION_COMMAND__PRE_ORIGINATE: _ClassVar[EventType]
140
+ CANCEL_PRESCRIPTION_COMMAND__POST_ORIGINATE: _ClassVar[EventType]
141
+ CANCEL_PRESCRIPTION_COMMAND__PRE_UPDATE: _ClassVar[EventType]
142
+ CANCEL_PRESCRIPTION_COMMAND__POST_UPDATE: _ClassVar[EventType]
143
+ CANCEL_PRESCRIPTION_COMMAND__PRE_COMMIT: _ClassVar[EventType]
144
+ CANCEL_PRESCRIPTION_COMMAND__POST_COMMIT: _ClassVar[EventType]
145
+ CANCEL_PRESCRIPTION_COMMAND__PRE_DELETE: _ClassVar[EventType]
146
+ CANCEL_PRESCRIPTION_COMMAND__POST_DELETE: _ClassVar[EventType]
147
+ CANCEL_PRESCRIPTION_COMMAND__PRE_ENTER_IN_ERROR: _ClassVar[EventType]
148
+ CANCEL_PRESCRIPTION_COMMAND__POST_ENTER_IN_ERROR: _ClassVar[EventType]
149
+ CANCEL_PRESCRIPTION_COMMAND__PRE_EXECUTE_ACTION: _ClassVar[EventType]
150
+ CANCEL_PRESCRIPTION_COMMAND__POST_EXECUTE_ACTION: _ClassVar[EventType]
151
+ CANCEL_PRESCRIPTION__SELECTED_PRESCRIPTION__PRE_SEARCH: _ClassVar[EventType]
152
+ CANCEL_PRESCRIPTION__SELECTED_PRESCRIPTION__POST_SEARCH: _ClassVar[EventType]
139
153
  CLIPBOARD_COMMAND__PRE_ORIGINATE: _ClassVar[EventType]
140
154
  CLIPBOARD_COMMAND__POST_ORIGINATE: _ClassVar[EventType]
141
155
  CLIPBOARD_COMMAND__PRE_UPDATE: _ClassVar[EventType]
@@ -503,6 +517,20 @@ class EventType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
503
517
  ROS_COMMAND__POST_INSERTED_INTO_NOTE: _ClassVar[EventType]
504
518
  ROS__QUESTIONNAIRE__PRE_SEARCH: _ClassVar[EventType]
505
519
  ROS__QUESTIONNAIRE__POST_SEARCH: _ClassVar[EventType]
520
+ SNOOZE_PROTOCOL_COMMAND__PRE_ORIGINATE: _ClassVar[EventType]
521
+ SNOOZE_PROTOCOL_COMMAND__POST_ORIGINATE: _ClassVar[EventType]
522
+ SNOOZE_PROTOCOL_COMMAND__PRE_UPDATE: _ClassVar[EventType]
523
+ SNOOZE_PROTOCOL_COMMAND__POST_UPDATE: _ClassVar[EventType]
524
+ SNOOZE_PROTOCOL_COMMAND__PRE_COMMIT: _ClassVar[EventType]
525
+ SNOOZE_PROTOCOL_COMMAND__POST_COMMIT: _ClassVar[EventType]
526
+ SNOOZE_PROTOCOL_COMMAND__PRE_DELETE: _ClassVar[EventType]
527
+ SNOOZE_PROTOCOL_COMMAND__POST_DELETE: _ClassVar[EventType]
528
+ SNOOZE_PROTOCOL_COMMAND__PRE_ENTER_IN_ERROR: _ClassVar[EventType]
529
+ SNOOZE_PROTOCOL_COMMAND__POST_ENTER_IN_ERROR: _ClassVar[EventType]
530
+ SNOOZE_PROTOCOL_COMMAND__PRE_EXECUTE_ACTION: _ClassVar[EventType]
531
+ SNOOZE_PROTOCOL_COMMAND__POST_EXECUTE_ACTION: _ClassVar[EventType]
532
+ SNOOZE_PROTOCOL__PROTOCOL__PRE_SEARCH: _ClassVar[EventType]
533
+ SNOOZE_PROTOCOL__PROTOCOL__POST_SEARCH: _ClassVar[EventType]
506
534
  STOP_MEDICATION_COMMAND__PRE_ORIGINATE: _ClassVar[EventType]
507
535
  STOP_MEDICATION_COMMAND__POST_ORIGINATE: _ClassVar[EventType]
508
536
  STOP_MEDICATION_COMMAND__PRE_UPDATE: _ClassVar[EventType]
@@ -565,6 +593,22 @@ class EventType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
565
593
  TASK__ASSIGN_TO__POST_SEARCH: _ClassVar[EventType]
566
594
  TASK__LABELS__PRE_SEARCH: _ClassVar[EventType]
567
595
  TASK__LABELS__POST_SEARCH: _ClassVar[EventType]
596
+ UPDATE_DIAGNOSIS_COMMAND__PRE_ORIGINATE: _ClassVar[EventType]
597
+ UPDATE_DIAGNOSIS_COMMAND__POST_ORIGINATE: _ClassVar[EventType]
598
+ UPDATE_DIAGNOSIS_COMMAND__PRE_UPDATE: _ClassVar[EventType]
599
+ UPDATE_DIAGNOSIS_COMMAND__POST_UPDATE: _ClassVar[EventType]
600
+ UPDATE_DIAGNOSIS_COMMAND__PRE_COMMIT: _ClassVar[EventType]
601
+ UPDATE_DIAGNOSIS_COMMAND__POST_COMMIT: _ClassVar[EventType]
602
+ UPDATE_DIAGNOSIS_COMMAND__PRE_DELETE: _ClassVar[EventType]
603
+ UPDATE_DIAGNOSIS_COMMAND__POST_DELETE: _ClassVar[EventType]
604
+ UPDATE_DIAGNOSIS_COMMAND__PRE_ENTER_IN_ERROR: _ClassVar[EventType]
605
+ UPDATE_DIAGNOSIS_COMMAND__POST_ENTER_IN_ERROR: _ClassVar[EventType]
606
+ UPDATE_DIAGNOSIS_COMMAND__PRE_EXECUTE_ACTION: _ClassVar[EventType]
607
+ UPDATE_DIAGNOSIS_COMMAND__POST_EXECUTE_ACTION: _ClassVar[EventType]
608
+ UPDATE_DIAGNOSIS__CONDITION__PRE_SEARCH: _ClassVar[EventType]
609
+ UPDATE_DIAGNOSIS__CONDITION__POST_SEARCH: _ClassVar[EventType]
610
+ UPDATE_DIAGNOSIS__NEW_CONDITION__PRE_SEARCH: _ClassVar[EventType]
611
+ UPDATE_DIAGNOSIS__NEW_CONDITION__POST_SEARCH: _ClassVar[EventType]
568
612
  UPDATE_GOAL_COMMAND__PRE_ORIGINATE: _ClassVar[EventType]
569
613
  UPDATE_GOAL_COMMAND__POST_ORIGINATE: _ClassVar[EventType]
570
614
  UPDATE_GOAL_COMMAND__PRE_UPDATE: _ClassVar[EventType]
@@ -810,6 +854,20 @@ ASSESS_COMMAND__PRE_EXECUTE_ACTION: EventType
810
854
  ASSESS_COMMAND__POST_EXECUTE_ACTION: EventType
811
855
  ASSESS__CONDITION__POST_SEARCH: EventType
812
856
  ASSESS__CONDITION__PRE_SEARCH: EventType
857
+ CANCEL_PRESCRIPTION_COMMAND__PRE_ORIGINATE: EventType
858
+ CANCEL_PRESCRIPTION_COMMAND__POST_ORIGINATE: EventType
859
+ CANCEL_PRESCRIPTION_COMMAND__PRE_UPDATE: EventType
860
+ CANCEL_PRESCRIPTION_COMMAND__POST_UPDATE: EventType
861
+ CANCEL_PRESCRIPTION_COMMAND__PRE_COMMIT: EventType
862
+ CANCEL_PRESCRIPTION_COMMAND__POST_COMMIT: EventType
863
+ CANCEL_PRESCRIPTION_COMMAND__PRE_DELETE: EventType
864
+ CANCEL_PRESCRIPTION_COMMAND__POST_DELETE: EventType
865
+ CANCEL_PRESCRIPTION_COMMAND__PRE_ENTER_IN_ERROR: EventType
866
+ CANCEL_PRESCRIPTION_COMMAND__POST_ENTER_IN_ERROR: EventType
867
+ CANCEL_PRESCRIPTION_COMMAND__PRE_EXECUTE_ACTION: EventType
868
+ CANCEL_PRESCRIPTION_COMMAND__POST_EXECUTE_ACTION: EventType
869
+ CANCEL_PRESCRIPTION__SELECTED_PRESCRIPTION__PRE_SEARCH: EventType
870
+ CANCEL_PRESCRIPTION__SELECTED_PRESCRIPTION__POST_SEARCH: EventType
813
871
  CLIPBOARD_COMMAND__PRE_ORIGINATE: EventType
814
872
  CLIPBOARD_COMMAND__POST_ORIGINATE: EventType
815
873
  CLIPBOARD_COMMAND__PRE_UPDATE: EventType
@@ -1177,6 +1235,20 @@ ROS_COMMAND__POST_EXECUTE_ACTION: EventType
1177
1235
  ROS_COMMAND__POST_INSERTED_INTO_NOTE: EventType
1178
1236
  ROS__QUESTIONNAIRE__PRE_SEARCH: EventType
1179
1237
  ROS__QUESTIONNAIRE__POST_SEARCH: EventType
1238
+ SNOOZE_PROTOCOL_COMMAND__PRE_ORIGINATE: EventType
1239
+ SNOOZE_PROTOCOL_COMMAND__POST_ORIGINATE: EventType
1240
+ SNOOZE_PROTOCOL_COMMAND__PRE_UPDATE: EventType
1241
+ SNOOZE_PROTOCOL_COMMAND__POST_UPDATE: EventType
1242
+ SNOOZE_PROTOCOL_COMMAND__PRE_COMMIT: EventType
1243
+ SNOOZE_PROTOCOL_COMMAND__POST_COMMIT: EventType
1244
+ SNOOZE_PROTOCOL_COMMAND__PRE_DELETE: EventType
1245
+ SNOOZE_PROTOCOL_COMMAND__POST_DELETE: EventType
1246
+ SNOOZE_PROTOCOL_COMMAND__PRE_ENTER_IN_ERROR: EventType
1247
+ SNOOZE_PROTOCOL_COMMAND__POST_ENTER_IN_ERROR: EventType
1248
+ SNOOZE_PROTOCOL_COMMAND__PRE_EXECUTE_ACTION: EventType
1249
+ SNOOZE_PROTOCOL_COMMAND__POST_EXECUTE_ACTION: EventType
1250
+ SNOOZE_PROTOCOL__PROTOCOL__PRE_SEARCH: EventType
1251
+ SNOOZE_PROTOCOL__PROTOCOL__POST_SEARCH: EventType
1180
1252
  STOP_MEDICATION_COMMAND__PRE_ORIGINATE: EventType
1181
1253
  STOP_MEDICATION_COMMAND__POST_ORIGINATE: EventType
1182
1254
  STOP_MEDICATION_COMMAND__PRE_UPDATE: EventType
@@ -1239,6 +1311,22 @@ TASK__ASSIGN_TO__PRE_SEARCH: EventType
1239
1311
  TASK__ASSIGN_TO__POST_SEARCH: EventType
1240
1312
  TASK__LABELS__PRE_SEARCH: EventType
1241
1313
  TASK__LABELS__POST_SEARCH: EventType
1314
+ UPDATE_DIAGNOSIS_COMMAND__PRE_ORIGINATE: EventType
1315
+ UPDATE_DIAGNOSIS_COMMAND__POST_ORIGINATE: EventType
1316
+ UPDATE_DIAGNOSIS_COMMAND__PRE_UPDATE: EventType
1317
+ UPDATE_DIAGNOSIS_COMMAND__POST_UPDATE: EventType
1318
+ UPDATE_DIAGNOSIS_COMMAND__PRE_COMMIT: EventType
1319
+ UPDATE_DIAGNOSIS_COMMAND__POST_COMMIT: EventType
1320
+ UPDATE_DIAGNOSIS_COMMAND__PRE_DELETE: EventType
1321
+ UPDATE_DIAGNOSIS_COMMAND__POST_DELETE: EventType
1322
+ UPDATE_DIAGNOSIS_COMMAND__PRE_ENTER_IN_ERROR: EventType
1323
+ UPDATE_DIAGNOSIS_COMMAND__POST_ENTER_IN_ERROR: EventType
1324
+ UPDATE_DIAGNOSIS_COMMAND__PRE_EXECUTE_ACTION: EventType
1325
+ UPDATE_DIAGNOSIS_COMMAND__POST_EXECUTE_ACTION: EventType
1326
+ UPDATE_DIAGNOSIS__CONDITION__PRE_SEARCH: EventType
1327
+ UPDATE_DIAGNOSIS__CONDITION__POST_SEARCH: EventType
1328
+ UPDATE_DIAGNOSIS__NEW_CONDITION__PRE_SEARCH: EventType
1329
+ UPDATE_DIAGNOSIS__NEW_CONDITION__POST_SEARCH: EventType
1242
1330
  UPDATE_GOAL_COMMAND__PRE_ORIGINATE: EventType
1243
1331
  UPDATE_GOAL_COMMAND__POST_ORIGINATE: EventType
1244
1332
  UPDATE_GOAL_COMMAND__PRE_UPDATE: EventType
@@ -1,8 +1,10 @@
1
1
  from collections.abc import Generator
2
2
  from datetime import datetime
3
+ from typing import cast
3
4
 
4
5
  import pytest
5
6
 
7
+ import settings
6
8
  from canvas_sdk.commands.tests.test_utils import (
7
9
  COMMANDS,
8
10
  MaskedValue,
@@ -12,6 +14,7 @@ from canvas_sdk.commands.tests.test_utils import (
12
14
  get_token,
13
15
  install_plugin,
14
16
  trigger_plugin_event,
17
+ wait_for_log,
15
18
  write_protocol_code,
16
19
  )
17
20
 
@@ -40,10 +43,18 @@ def write_and_install_protocol_and_clean_up(
40
43
  ) -> Generator[None, None, None]:
41
44
  """Write the protocol code, install the plugin, and clean up after the test."""
42
45
  write_protocol_code(new_note["externallyExposableId"], plugin_name, COMMANDS)
46
+ message_received_event, thread, ws = wait_for_log(
47
+ cast(str, settings.INTEGRATION_TEST_URL),
48
+ token.value,
49
+ f"Loading plugin '{plugin_name}",
50
+ )
43
51
  install_plugin(plugin_name, token)
52
+ message_received_event.wait(timeout=5.0)
44
53
 
45
54
  yield
46
55
 
56
+ ws.close()
57
+ thread.join()
47
58
  clean_up_files_and_plugins(plugin_name, token)
48
59
 
49
60
 
@@ -57,8 +68,7 @@ def test_protocol_that_inserts_every_command(
57
68
  commands_in_body = get_original_note_body_commands(new_note["id"], token)
58
69
 
59
70
  # TODO: Temporary workaround to ignore the updateGoal command until the integration test instance is fixed.
60
- command_keys = [c.Meta.key for c in COMMANDS if c.Meta.key != "updateGoal"]
61
-
71
+ command_keys = [c.Meta.key for c in COMMANDS]
62
72
  assert len(command_keys) == len(commands_in_body)
63
73
  for i, command_key in enumerate(command_keys):
64
74
  assert commands_in_body[i] == command_key
@@ -190,11 +190,6 @@ class Protocol(BaseProtocol):
190
190
  def install_plugin(plugin_name: str, token: MaskedValue) -> None:
191
191
  """Install a plugin."""
192
192
  with open(_build_package(Path(f"./custom-plugins/{plugin_name}")), "rb") as package:
193
- message_received_event = wait_for_log(
194
- cast(str, settings.INTEGRATION_TEST_URL),
195
- token.value,
196
- f"Loading plugin '{plugin_name}",
197
- )
198
193
  response = requests.post(
199
194
  plugin_url(cast(str, settings.INTEGRATION_TEST_URL)),
200
195
  data={"is_enabled": True},
@@ -203,8 +198,6 @@ def install_plugin(plugin_name: str, token: MaskedValue) -> None:
203
198
  )
204
199
  response.raise_for_status()
205
200
 
206
- message_received_event.wait(timeout=5.0)
207
-
208
201
 
209
202
  def trigger_plugin_event(token: MaskedValue) -> None:
210
203
  """Trigger a plugin event."""
@@ -328,7 +321,9 @@ def get_token() -> MaskedValue:
328
321
  return MaskedValue(response.json()["access_token"])
329
322
 
330
323
 
331
- def wait_for_log(host: str, token: str, message: str) -> threading.Event:
324
+ def wait_for_log(
325
+ host: str, token: str, message: str
326
+ ) -> tuple[threading.Event, threading.Thread, websocket.WebSocketApp]:
332
327
  """Wait for a specific log message."""
333
328
  hostname = cast(str, urlparse(host).hostname)
334
329
  instance = hostname.removesuffix(".canvasmedical.com")
@@ -362,4 +357,4 @@ def wait_for_log(host: str, token: str, message: str) -> threading.Event:
362
357
 
363
358
  connected_event.wait(timeout=5.0)
364
359
 
365
- return message_received_event
360
+ return message_received_event, thread, ws
@@ -92,10 +92,10 @@ class Protocol(BaseProtocol):
92
92
  protocol.write(protocol_code)
93
93
 
94
94
  with open(_build_package(Path(f"./custom-plugins/{plugin_name}")), "rb") as package:
95
- message_received_event = wait_for_log(
95
+ message_received_event, thread, ws = wait_for_log(
96
96
  settings.INTEGRATION_TEST_URL,
97
97
  token.value,
98
- f"Loading plugin '{plugin_name}:{plugin_name}.protocols.my_protocol:Protocol'",
98
+ f"Loading plugin '{plugin_name}",
99
99
  )
100
100
 
101
101
  # install the plugin
@@ -111,6 +111,9 @@ class Protocol(BaseProtocol):
111
111
 
112
112
  yield
113
113
 
114
+ ws.close()
115
+ thread.join()
116
+
114
117
  # clean up
115
118
  if Path(f"./custom-plugins/{plugin_name}").exists():
116
119
  shutil.rmtree(Path(f"./custom-plugins/{plugin_name}"))
@@ -0,0 +1,11 @@
1
+ from .surescripts_messages import (
2
+ SendSurescriptsBenefitsRequestEffect,
3
+ SendSurescriptsEligibilityRequestEffect,
4
+ SendSurescriptsMedicationHistoryRequestEffect,
5
+ )
6
+
7
+ __all__ = [
8
+ "SendSurescriptsBenefitsRequestEffect",
9
+ "SendSurescriptsEligibilityRequestEffect",
10
+ "SendSurescriptsMedicationHistoryRequestEffect",
11
+ ]
@@ -0,0 +1,3 @@
1
+ from .task import AddTask, AddTaskComment, TaskStatus, UpdateTask
2
+
3
+ __all__ = ["AddTask", "AddTaskComment", "TaskStatus", "UpdateTask"]
@@ -23,6 +23,7 @@ class AddTask(_BaseEffect):
23
23
  apply_required_fields = ("title",)
24
24
 
25
25
  assignee_id: str | None = None
26
+ team_id: str | None = None
26
27
  patient_id: str | None = None
27
28
  title: str | None = None
28
29
  due: datetime | None = None
@@ -36,6 +37,7 @@ class AddTask(_BaseEffect):
36
37
  "patient": {"id": self.patient_id},
37
38
  "due": self.due.isoformat() if self.due else None,
38
39
  "assignee": {"id": self.assignee_id},
40
+ "team": {"id": self.team_id},
39
41
  "title": self.title,
40
42
  "status": self.status.value,
41
43
  "labels": self.labels,
@@ -74,6 +76,7 @@ class UpdateTask(_BaseEffect):
74
76
 
75
77
  id: str | None = None
76
78
  assignee_id: str | None = None
79
+ team_id: str | None = None
77
80
  patient_id: str | None = None
78
81
  title: str | None = None
79
82
  due: datetime | None = None
@@ -1,3 +1,9 @@
1
- from canvas_sdk.utils.http import Http
1
+ from canvas_sdk.utils.http import Http, batch_get, batch_patch, batch_post, batch_put
2
2
 
3
- __all__ = ("Http",)
3
+ __all__ = (
4
+ "Http",
5
+ "batch_get",
6
+ "batch_patch",
7
+ "batch_post",
8
+ "batch_put",
9
+ )
canvas_sdk/utils/http.py CHANGED
@@ -1,7 +1,10 @@
1
+ import concurrent
2
+ import functools
1
3
  import time
2
- from collections.abc import Callable, Mapping
4
+ from collections.abc import Callable, Iterable, Mapping
5
+ from concurrent.futures import ThreadPoolExecutor
3
6
  from functools import wraps
4
- from typing import Any, TypeVar, cast
7
+ from typing import Any, Literal, Protocol, TypeVar, cast
5
8
 
6
9
  import requests
7
10
  import statsd
@@ -9,9 +12,90 @@ import statsd
9
12
  F = TypeVar("F", bound=Callable)
10
13
 
11
14
 
15
+ class _BatchableRequest:
16
+ """Representation of a request that will be executed in parallel with other requests."""
17
+
18
+ def __init__(
19
+ self, method: Literal["GET", "POST", "PUT", "PATCH"], url: str, **kwargs: Any
20
+ ) -> None:
21
+ self._method = method
22
+ self._url = url
23
+ self._kwargs = kwargs
24
+
25
+ def fn(self, client: "Http") -> Callable:
26
+ """
27
+ Return a callable constructed from an Http object and the method, URL, and kwargs.
28
+
29
+ The callable is passed to the ThreadPoolExecutor.
30
+ """
31
+ client_method: Callable
32
+ match self._method:
33
+ case "GET":
34
+ client_method = client.get
35
+ case "POST":
36
+ client_method = client.post
37
+ case "PUT":
38
+ client_method = client.put
39
+ case "PATCH":
40
+ client_method = client.patch
41
+ case _:
42
+ raise ValueError(f"HTTP method {self._method} is not supported")
43
+
44
+ return functools.partial(client_method, self._url, **self._kwargs)
45
+
46
+
47
+ class BatchableRequest(Protocol):
48
+ """Protocol for batchable requests."""
49
+
50
+ def fn(self, client: "Http") -> Callable:
51
+ """
52
+ Return a callable that can be passed to the ThreadPoolExecutor.
53
+ """
54
+ ...
55
+
56
+
57
+ def batch_get(
58
+ url: str, headers: Mapping[str, str | bytes | None] | None = None
59
+ ) -> BatchableRequest:
60
+ """Return a batchable GET request."""
61
+ return _BatchableRequest("GET", url, headers=headers)
62
+
63
+
64
+ def batch_post(
65
+ url: str,
66
+ json: dict | None = None,
67
+ data: dict | str | list | bytes | None = None,
68
+ headers: Mapping[str, str | bytes | None] | None = None,
69
+ ) -> BatchableRequest:
70
+ """Return a batchable POST request."""
71
+ return _BatchableRequest("POST", url, json=json, data=data, headeres=headers)
72
+
73
+
74
+ def batch_put(
75
+ url: str,
76
+ json: dict | None = None,
77
+ data: dict | str | list | bytes | None = None,
78
+ headers: Mapping[str, str | bytes | None] | None = None,
79
+ ) -> BatchableRequest:
80
+ """Return a batchable PUT request."""
81
+ return _BatchableRequest("PUT", url, json=json, data=data, headers=headers)
82
+
83
+
84
+ def batch_patch(
85
+ url: str,
86
+ json: dict | None = None,
87
+ data: dict | str | list | bytes | None = None,
88
+ headers: Mapping[str, str | bytes | None] | None = None,
89
+ ) -> BatchableRequest:
90
+ """Return a batchable PATCH request."""
91
+ return _BatchableRequest("PATCH", url, json=json, data=data, headers=headers)
92
+
93
+
12
94
  class Http:
13
95
  """A helper class for completing HTTP calls with metrics tracking."""
14
96
 
97
+ _MAX_WORKER_TIMEOUT_SECONDS = 30
98
+
15
99
  def __init__(self) -> None:
16
100
  self.session = requests.Session()
17
101
  self.statsd_client = statsd.StatsClient()
@@ -26,7 +110,7 @@ class Http:
26
110
  result = fn(self, *args, **kwargs)
27
111
  end_time = time.time()
28
112
  timing = int((end_time - start_time) * 1000)
29
- self.statsd_client.timing(f"http_{fn.__name__}", timing)
113
+ self.statsd_client.timing(f"plugins.http_{fn.__name__}", timing)
30
114
  return result
31
115
 
32
116
  return cast(F, wrapper)
@@ -72,3 +156,29 @@ class Http:
72
156
  ) -> requests.Response:
73
157
  """Sends a PATCH request."""
74
158
  return self.session.patch(url, json=json, data=data, headers=headers)
159
+
160
+ @measure_time
161
+ def batch_requests(
162
+ self,
163
+ batch_requests: Iterable[BatchableRequest],
164
+ timeout: int | None = None,
165
+ ) -> list[requests.Response]:
166
+ """
167
+ Execute requests in parallel.
168
+
169
+ Wait for the responses to complete, and then return a list of the responses in the same
170
+ ordering as the requests.
171
+ """
172
+ if timeout is None:
173
+ timeout = self._MAX_WORKER_TIMEOUT_SECONDS
174
+ elif timeout < 1 or timeout > self._MAX_WORKER_TIMEOUT_SECONDS:
175
+ raise ValueError(
176
+ f"Timeout value must be greater than 0 and less than or equal to {self._MAX_WORKER_TIMEOUT_SECONDS} seconds"
177
+ )
178
+
179
+ with ThreadPoolExecutor() as executor:
180
+ futures = [executor.submit(request.fn(self)) for request in batch_requests]
181
+
182
+ concurrent.futures.wait(futures, timeout=timeout)
183
+
184
+ return [future.result() for future in futures]
@@ -0,0 +1,76 @@
1
+ from django.contrib.postgres.fields import ArrayField
2
+ from django.db import models
3
+
4
+ from canvas_sdk.v1.data.common import ContactPointState, ContactPointSystem, ContactPointUse
5
+
6
+
7
+ class TeamResponsibility(models.TextChoices):
8
+ """TeamResponsibility."""
9
+
10
+ COLLECT_SPECIMENS_FROM_PATIENT = (
11
+ "COLLECT_SPECIMENS_FROM_PATIENT",
12
+ "Collect specimens from a patient",
13
+ )
14
+ COMMUNICATE_DIAGNOSTIC_RESULTS_TO_PATIENT = (
15
+ "COMMUNICATE_DIAGNOSTIC_RESULTS_TO_PATIENT",
16
+ "Communicate diagnostic results to patient",
17
+ )
18
+ COORDINATE_REFERRALS_FOR_PATIENT = (
19
+ "COORDINATE_REFERRALS_FOR_PATIENT",
20
+ "Coordinate referrals for a patient",
21
+ )
22
+ PROCESS_REFILL_REQUESTS = "PROCESS_REFILL_REQUESTS", "Process refill requests from a pharmacy"
23
+ PROCESS_CHANGE_REQUESTS = "PROCESS_CHANGE_REQUESTS", "Process change requests from a pharmacy"
24
+ SCHEDULE_LAB_VISITS_FOR_PATIENT = (
25
+ "SCHEDULE_LAB_VISITS_FOR_PATIENT",
26
+ "Schedule lab visits for a patient",
27
+ )
28
+ POPULATION_HEALTH_CAMPAIGN_OUTREACH = (
29
+ "POPULATION_HEALTH_CAMPAIGN_OUTREACH",
30
+ "Population health campaign outreach",
31
+ )
32
+ COLLECT_PATIENT_PAYMENTS = "COLLECT_PATIENT_PAYMENTS", "Collect patient payments"
33
+ COMPLETE_OPEN_LAB_ORDERS = "COMPLETE_OPEN_LAB_ORDERS", "Complete open lab orders"
34
+ REVIEW_ERA_POSTING_EXCEPTIONS = (
35
+ "REVIEW_ERA_POSTING_EXCEPTIONS",
36
+ "Review electronic remittance posting exceptions",
37
+ )
38
+ REVIEW_COVERAGES = "REVIEW_COVERAGES", "Review incomplete patient coverages"
39
+
40
+
41
+ class Team(models.Model):
42
+ """Team."""
43
+
44
+ class Meta:
45
+ managed = False
46
+ db_table = "canvas_sdk_data_api_team_001"
47
+
48
+ id = models.UUIDField()
49
+ dbid = models.BigIntegerField(primary_key=True)
50
+ created = models.DateTimeField()
51
+ modified = models.DateTimeField()
52
+ name = models.CharField()
53
+ responsibilities = ArrayField(models.CharField(choices=TeamResponsibility.choices))
54
+ members = models.ManyToManyField( # type: ignore[var-annotated]
55
+ "v1.Staff", # type: ignore[misc]
56
+ related_name="teams",
57
+ db_table="canvas_sdk_data_api_team_members_001",
58
+ )
59
+
60
+
61
+ class TeamContactPoint(models.Model):
62
+ """TeamContactPoint."""
63
+
64
+ class Meta:
65
+ managed = False
66
+ db_table = "canvas_sdk_data_api_teamcontactpoint_001"
67
+
68
+ id = models.UUIDField()
69
+ dbid = models.BigIntegerField(primary_key=True)
70
+ system = models.CharField(choices=ContactPointSystem.choices)
71
+ value = models.CharField()
72
+ use = models.CharField(choices=ContactPointUse.choices)
73
+ use_notes = models.CharField()
74
+ rank = models.IntegerField()
75
+ state = models.CharField(choices=ContactPointState.choices)
76
+ team = models.ForeignKey(Team, on_delete=models.DO_NOTHING, related_name="telecom")
@@ -14,9 +14,17 @@ import requests
14
14
  from psycopg import Connection
15
15
  from psycopg.rows import dict_row
16
16
 
17
- import settings
18
17
  from plugin_runner.aws_headers import aws_sig_v4_headers
19
18
  from plugin_runner.exceptions import InvalidPluginFormat, PluginInstallationError
19
+ from settings import (
20
+ AWS_ACCESS_KEY_ID,
21
+ AWS_REGION,
22
+ AWS_SECRET_ACCESS_KEY,
23
+ CUSTOMER_IDENTIFIER,
24
+ MEDIA_S3_BUCKET_NAME,
25
+ PLUGIN_DIRECTORY,
26
+ SECRETS_FILE_NAME,
27
+ )
20
28
 
21
29
  # Plugin "packages" include this prefix in the database record for the plugin and the S3 bucket key.
22
30
  UPLOAD_TO_PREFIX = "plugins"
@@ -100,19 +108,19 @@ def _extract_rows_to_dict(rows: list) -> dict[str, PluginAttributes]:
100
108
  def download_plugin(plugin_package: str) -> Generator[Path, None, None]:
101
109
  """Download the plugin package from the S3 bucket."""
102
110
  method = "GET"
103
- host = f"s3-{settings.AWS_REGION}.amazonaws.com"
104
- bucket = settings.MEDIA_S3_BUCKET_NAME
105
- customer_identifier = settings.CUSTOMER_IDENTIFIER
111
+ host = f"s3-{AWS_REGION}.amazonaws.com"
112
+ bucket = MEDIA_S3_BUCKET_NAME
113
+ customer_identifier = CUSTOMER_IDENTIFIER
106
114
  path = f"/{bucket}/{customer_identifier}/{plugin_package}"
107
115
  payload = b"This is required for the AWS headers because it is part of the signature"
108
116
  pre_auth_headers: dict[str, str] = {}
109
117
  query: dict[str, str] = {}
110
118
  headers = aws_sig_v4_headers(
111
- settings.AWS_ACCESS_KEY_ID,
112
- settings.AWS_SECRET_ACCESS_KEY,
119
+ AWS_ACCESS_KEY_ID,
120
+ AWS_SECRET_ACCESS_KEY,
113
121
  pre_auth_headers,
114
122
  "s3",
115
- settings.AWS_REGION,
123
+ AWS_REGION,
116
124
  host,
117
125
  method,
118
126
  path,
@@ -135,7 +143,7 @@ def install_plugin(plugin_name: str, attributes: PluginAttributes) -> None:
135
143
  try:
136
144
  print(f"Installing plugin '{plugin_name}'")
137
145
 
138
- plugin_installation_path = Path(settings.PLUGIN_DIRECTORY) / plugin_name
146
+ plugin_installation_path = Path(PLUGIN_DIRECTORY) / plugin_name
139
147
 
140
148
  # if plugin exists, first uninstall it
141
149
  if plugin_installation_path.exists():
@@ -175,7 +183,7 @@ def install_plugin_secrets(plugin_name: str, secrets: dict[str, str]) -> None:
175
183
  """Write the plugin's secrets to disk in the package's directory."""
176
184
  print(f"Writing plugin secrets for '{plugin_name}'")
177
185
 
178
- secrets_path = Path(settings.PLUGIN_DIRECTORY) / plugin_name / settings.SECRETS_FILE_NAME
186
+ secrets_path = Path(PLUGIN_DIRECTORY) / plugin_name / SECRETS_FILE_NAME
179
187
 
180
188
  # Did the plugin ship a secrets.json? TOO BAD, IT'S GONE NOW.
181
189
  if Path(secrets_path).exists():
@@ -199,7 +207,7 @@ def disable_plugin(plugin_name: str) -> None:
199
207
 
200
208
  def uninstall_plugin(plugin_name: str) -> None:
201
209
  """Remove the plugin from the filesystem."""
202
- plugin_path = Path(settings.PLUGIN_DIRECTORY) / plugin_name
210
+ plugin_path = Path(PLUGIN_DIRECTORY) / plugin_name
203
211
 
204
212
  if plugin_path.exists():
205
213
  shutil.rmtree(plugin_path)
@@ -207,10 +215,10 @@ def uninstall_plugin(plugin_name: str) -> None:
207
215
 
208
216
  def install_plugins() -> None:
209
217
  """Install all enabled plugins."""
210
- if Path(settings.PLUGIN_DIRECTORY).exists():
211
- shutil.rmtree(settings.PLUGIN_DIRECTORY)
218
+ if Path(PLUGIN_DIRECTORY).exists():
219
+ shutil.rmtree(PLUGIN_DIRECTORY)
212
220
 
213
- os.mkdir(settings.PLUGIN_DIRECTORY)
221
+ os.mkdir(PLUGIN_DIRECTORY)
214
222
 
215
223
  for plugin_name, attributes in enabled_plugins().items():
216
224
  try: