canvas 0.2.5__py3-none-any.whl → 0.2.11__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 (86) hide show
  1. {canvas-0.2.5.dist-info → canvas-0.2.11.dist-info}/METADATA +4 -1
  2. canvas-0.2.11.dist-info/RECORD +144 -0
  3. canvas_cli/apps/plugin/plugin.py +51 -9
  4. canvas_cli/apps/plugin/tests.py +51 -0
  5. canvas_cli/tests.py +193 -4
  6. canvas_cli/utils/validators/manifest_schema.py +1 -0
  7. canvas_generated/messages/effects_pb2.py +2 -2
  8. canvas_generated/messages/effects_pb2.pyi +136 -0
  9. canvas_generated/messages/events_pb2.py +3 -3
  10. canvas_generated/messages/events_pb2.pyi +614 -0
  11. canvas_sdk/__init__.py +7 -0
  12. canvas_sdk/base.py +6 -2
  13. canvas_sdk/commands/__init__.py +26 -0
  14. canvas_sdk/commands/base.py +35 -32
  15. canvas_sdk/commands/commands/allergy.py +49 -0
  16. canvas_sdk/commands/commands/assess.py +1 -1
  17. canvas_sdk/commands/commands/close_goal.py +22 -0
  18. canvas_sdk/commands/commands/diagnose.py +3 -3
  19. canvas_sdk/commands/commands/family_history.py +18 -0
  20. canvas_sdk/commands/commands/goal.py +3 -3
  21. canvas_sdk/commands/commands/history_present_illness.py +1 -1
  22. canvas_sdk/commands/commands/instruct.py +17 -0
  23. canvas_sdk/commands/commands/lab_order.py +33 -0
  24. canvas_sdk/commands/commands/medical_history.py +34 -0
  25. canvas_sdk/commands/commands/medication_statement.py +1 -1
  26. canvas_sdk/commands/commands/past_surgical_history.py +28 -0
  27. canvas_sdk/commands/commands/perform.py +17 -0
  28. canvas_sdk/commands/commands/plan.py +2 -2
  29. canvas_sdk/commands/commands/prescribe.py +10 -7
  30. canvas_sdk/commands/commands/questionnaire.py +1 -1
  31. canvas_sdk/commands/commands/refill.py +16 -0
  32. canvas_sdk/commands/commands/remove_allergy.py +26 -0
  33. canvas_sdk/commands/commands/stop_medication.py +1 -1
  34. canvas_sdk/commands/commands/task.py +52 -0
  35. canvas_sdk/commands/commands/update_diagnosis.py +27 -0
  36. canvas_sdk/commands/commands/update_goal.py +1 -1
  37. canvas_sdk/commands/commands/vitals.py +78 -0
  38. canvas_sdk/commands/constants.py +7 -0
  39. canvas_sdk/commands/tests/protocol/__init__.py +0 -0
  40. canvas_sdk/commands/tests/protocol/tests.py +55 -0
  41. canvas_sdk/commands/tests/schema/__init__.py +0 -0
  42. canvas_sdk/commands/tests/schema/tests.py +104 -0
  43. canvas_sdk/commands/tests/test_utils.py +170 -6
  44. canvas_sdk/commands/tests/unit/__init__.py +0 -0
  45. canvas_sdk/commands/tests/{tests.py → unit/tests.py} +20 -194
  46. canvas_sdk/effects/banner_alert/add_banner_alert.py +8 -7
  47. canvas_sdk/effects/banner_alert/remove_banner_alert.py +3 -2
  48. canvas_sdk/effects/banner_alert/tests.py +224 -0
  49. canvas_sdk/effects/base.py +3 -5
  50. canvas_sdk/effects/patient_chart_summary_configuration.py +39 -0
  51. canvas_sdk/effects/protocol_card/__init__.py +1 -0
  52. canvas_sdk/effects/protocol_card/protocol_card.py +83 -0
  53. canvas_sdk/effects/protocol_card/tests.py +184 -0
  54. canvas_sdk/protocols/clinical_quality_measure.py +41 -0
  55. canvas_sdk/utils/db.py +17 -0
  56. canvas_sdk/v1/__init__.py +0 -0
  57. canvas_sdk/v1/data/__init__.py +3 -0
  58. canvas_sdk/v1/data/allergy_intolerance.py +63 -0
  59. canvas_sdk/v1/data/base.py +47 -0
  60. canvas_sdk/v1/data/condition.py +48 -0
  61. canvas_sdk/v1/data/lab.py +96 -0
  62. canvas_sdk/v1/data/medication.py +54 -0
  63. canvas_sdk/v1/data/patient.py +49 -0
  64. canvas_sdk/v1/data/user.py +10 -0
  65. canvas_sdk/value_set/tests/test_value_sets.py +65 -0
  66. canvas_sdk/value_set/v2022/adverse_event.py +33 -0
  67. canvas_sdk/value_set/v2022/allergy.py +232 -0
  68. canvas_sdk/value_set/v2022/assessment.py +215 -0
  69. canvas_sdk/value_set/v2022/communication.py +325 -0
  70. canvas_sdk/value_set/v2022/condition.py +40654 -0
  71. canvas_sdk/value_set/v2022/device.py +174 -0
  72. canvas_sdk/value_set/v2022/diagnostic_study.py +4967 -0
  73. canvas_sdk/value_set/v2022/encounter.py +2564 -0
  74. canvas_sdk/value_set/v2022/immunization.py +341 -0
  75. canvas_sdk/value_set/v2022/individual_characteristic.py +307 -0
  76. canvas_sdk/value_set/v2022/intervention.py +1356 -0
  77. canvas_sdk/value_set/v2022/laboratory_test.py +1250 -0
  78. canvas_sdk/value_set/v2022/medication.py +5130 -0
  79. canvas_sdk/value_set/v2022/physical_exam.py +201 -0
  80. canvas_sdk/value_set/v2022/procedure.py +4037 -0
  81. canvas_sdk/value_set/v2022/symptom.py +176 -0
  82. canvas_sdk/value_set/value_set.py +91 -0
  83. settings.py +43 -0
  84. canvas-0.2.5.dist-info/RECORD +0 -91
  85. {canvas-0.2.5.dist-info → canvas-0.2.11.dist-info}/WHEEL +0 -0
  86. {canvas-0.2.5.dist-info → canvas-0.2.11.dist-info}/entry_points.txt +0 -0
@@ -1,11 +1,7 @@
1
- import decimal
2
- from datetime import datetime
3
-
4
1
  import pytest
5
- import requests
6
2
  from pydantic import ValidationError
3
+ from typer.testing import CliRunner
7
4
 
8
- import settings
9
5
  from canvas_sdk.commands import (
10
6
  AssessCommand,
11
7
  DiagnoseCommand,
@@ -19,15 +15,16 @@ from canvas_sdk.commands import (
19
15
  StopMedicationCommand,
20
16
  UpdateGoalCommand,
21
17
  )
22
- from canvas_sdk.commands.constants import Coding
18
+ from canvas_sdk.commands.base import _BaseCommand
23
19
  from canvas_sdk.commands.tests.test_utils import (
24
- MaskedValue,
25
20
  fake,
26
21
  get_field_type,
27
22
  raises_none_error_for_effect_method,
28
23
  raises_wrong_type_error,
29
24
  )
30
25
 
26
+ runner = CliRunner()
27
+
31
28
 
32
29
  @pytest.mark.parametrize(
33
30
  "Command,fields_to_test",
@@ -50,7 +47,7 @@ from canvas_sdk.commands.tests.test_utils import (
50
47
  ),
51
48
  (HistoryOfPresentIllnessCommand, ("narrative",)),
52
49
  (MedicationStatementCommand, ("fdb_code", "sig")),
53
- (PlanCommand, ("narrative", "user_id", "command_uuid")),
50
+ (PlanCommand, ("narrative", "command_uuid")),
54
51
  (
55
52
  PrescribeCommand,
56
53
  (
@@ -82,19 +79,7 @@ from canvas_sdk.commands.tests.test_utils import (
82
79
  ],
83
80
  )
84
81
  def test_command_raises_generic_error_when_kwarg_given_incorrect_type(
85
- Command: (
86
- AssessCommand
87
- | DiagnoseCommand
88
- | GoalCommand
89
- | HistoryOfPresentIllnessCommand
90
- | MedicationStatementCommand
91
- | PlanCommand
92
- | PrescribeCommand
93
- | QuestionnaireCommand
94
- | ReasonForVisitCommand
95
- | StopMedicationCommand
96
- | UpdateGoalCommand
97
- ),
82
+ Command: _BaseCommand,
98
83
  fields_to_test: tuple[str],
99
84
  ) -> None:
100
85
  for field in fields_to_test:
@@ -109,29 +94,22 @@ def test_command_raises_generic_error_when_kwarg_given_incorrect_type(
109
94
  [
110
95
  (
111
96
  PlanCommand,
112
- {"narrative": "yo", "user_id": 5, "note_uuid": 1},
97
+ {"narrative": "yo", "note_uuid": 1},
113
98
  "1 validation error for PlanCommand\nnote_uuid\n Input should be a valid string [type=string_type",
114
- {"narrative": "yo", "note_uuid": "00000000-0000-0000-0000-000000000000", "user_id": 1},
99
+ {"narrative": "yo", "note_uuid": "00000000-0000-0000-0000-000000000000"},
115
100
  ),
116
101
  (
117
102
  PlanCommand,
118
- {"narrative": "yo", "user_id": 5, "note_uuid": "5", "command_uuid": 5},
103
+ {"narrative": "yo", "note_uuid": "5", "command_uuid": 5},
119
104
  "1 validation error for PlanCommand\ncommand_uuid\n Input should be a valid string [type=string_type",
120
- {"narrative": "yo", "user_id": 5, "note_uuid": "5", "command_uuid": "5"},
121
- ),
122
- (
123
- PlanCommand,
124
- {"narrative": "yo", "note_uuid": "5", "command_uuid": "4", "user_id": "5"},
125
- "1 validation error for PlanCommand\nuser_id\n Input should be a valid integer [type=int_type",
126
- {"narrative": "yo", "note_uuid": "5", "command_uuid": "4", "user_id": 5},
105
+ {"narrative": "yo", "note_uuid": "5", "command_uuid": "5"},
127
106
  ),
128
107
  (
129
108
  ReasonForVisitCommand,
130
- {"note_uuid": "00000000-0000-0000-0000-000000000000", "user_id": 1, "structured": True},
109
+ {"note_uuid": "00000000-0000-0000-0000-000000000000", "structured": True},
131
110
  "1 validation error for ReasonForVisitCommand\n Structured RFV should have a coding",
132
111
  {
133
112
  "note_uuid": "00000000-0000-0000-0000-000000000000",
134
- "user_id": 1,
135
113
  "structured": False,
136
114
  },
137
115
  ),
@@ -139,71 +117,64 @@ def test_command_raises_generic_error_when_kwarg_given_incorrect_type(
139
117
  ReasonForVisitCommand,
140
118
  {
141
119
  "note_uuid": "00000000-0000-0000-0000-000000000000",
142
- "user_id": 1,
143
120
  "coding": {"code": "x"},
144
121
  },
145
122
  "1 validation error for ReasonForVisitCommand\ncoding.system\n Field required [type=missing",
146
- {"note_uuid": "00000000-0000-0000-0000-000000000000", "user_id": 1},
123
+ {"note_uuid": "00000000-0000-0000-0000-000000000000"},
147
124
  ),
148
125
  (
149
126
  ReasonForVisitCommand,
150
127
  {
151
128
  "note_uuid": "00000000-0000-0000-0000-000000000000",
152
- "user_id": 1,
153
129
  "coding": {"code": 1, "system": "y"},
154
130
  },
155
131
  "1 validation error for ReasonForVisitCommand\ncoding.code\n Input should be a valid string [type=string_type",
156
- {"note_uuid": "00000000-0000-0000-0000-000000000000", "user_id": 1},
132
+ {"note_uuid": "00000000-0000-0000-0000-000000000000"},
157
133
  ),
158
134
  (
159
135
  ReasonForVisitCommand,
160
136
  {
161
137
  "note_uuid": "00000000-0000-0000-0000-000000000000",
162
- "user_id": 1,
163
138
  "coding": {"code": None, "system": "y"},
164
139
  },
165
140
  "1 validation error for ReasonForVisitCommand\ncoding.code\n Input should be a valid string [type=string_type",
166
- {"note_uuid": "00000000-0000-0000-0000-000000000000", "user_id": 1},
141
+ {"note_uuid": "00000000-0000-0000-0000-000000000000"},
167
142
  ),
168
143
  (
169
144
  ReasonForVisitCommand,
170
145
  {
171
146
  "note_uuid": "00000000-0000-0000-0000-000000000000",
172
- "user_id": 1,
173
147
  "coding": {"system": "y"},
174
148
  },
175
149
  "1 validation error for ReasonForVisitCommand\ncoding.code\n Field required [type=missing",
176
- {"note_uuid": "00000000-0000-0000-0000-000000000000", "user_id": 1},
150
+ {"note_uuid": "00000000-0000-0000-0000-000000000000"},
177
151
  ),
178
152
  (
179
153
  ReasonForVisitCommand,
180
154
  {
181
155
  "note_uuid": "00000000-0000-0000-0000-000000000000",
182
- "user_id": 1,
183
156
  "coding": {"code": "x", "system": 1},
184
157
  },
185
158
  "1 validation error for ReasonForVisitCommand\ncoding.system\n Input should be a valid string [type=string_type",
186
- {"note_uuid": "00000000-0000-0000-0000-000000000000", "user_id": 1},
159
+ {"note_uuid": "00000000-0000-0000-0000-000000000000"},
187
160
  ),
188
161
  (
189
162
  ReasonForVisitCommand,
190
163
  {
191
164
  "note_uuid": "00000000-0000-0000-0000-000000000000",
192
- "user_id": 1,
193
165
  "coding": {"code": "x", "system": None},
194
166
  },
195
167
  "1 validation error for ReasonForVisitCommand\ncoding.system\n Input should be a valid string [type=string_type",
196
- {"note_uuid": "00000000-0000-0000-0000-000000000000", "user_id": 1},
168
+ {"note_uuid": "00000000-0000-0000-0000-000000000000"},
197
169
  ),
198
170
  (
199
171
  ReasonForVisitCommand,
200
172
  {
201
173
  "note_uuid": "00000000-0000-0000-0000-000000000000",
202
- "user_id": 1,
203
174
  "coding": {"code": "x", "system": "y", "display": 1},
204
175
  },
205
176
  "1 validation error for ReasonForVisitCommand\ncoding.display\n Input should be a valid string [type=string_type",
206
- {"note_uuid": "00000000-0000-0000-0000-000000000000", "user_id": 1},
177
+ {"note_uuid": "00000000-0000-0000-0000-000000000000"},
207
178
  ),
208
179
  ],
209
180
  )
@@ -251,7 +222,7 @@ def test_command_raises_specific_error_when_kwarg_given_incorrect_type(
251
222
  ),
252
223
  (HistoryOfPresentIllnessCommand, ("narrative",)),
253
224
  (MedicationStatementCommand, ("fdb_code", "sig")),
254
- (PlanCommand, ("narrative", "user_id", "command_uuid", "note_uuid")),
225
+ (PlanCommand, ("narrative", "command_uuid", "note_uuid")),
255
226
  (
256
227
  PrescribeCommand,
257
228
  (
@@ -284,19 +255,7 @@ def test_command_raises_specific_error_when_kwarg_given_incorrect_type(
284
255
  ],
285
256
  )
286
257
  def test_command_allows_kwarg_with_correct_type(
287
- Command: (
288
- AssessCommand
289
- | DiagnoseCommand
290
- | GoalCommand
291
- | HistoryOfPresentIllnessCommand
292
- | MedicationStatementCommand
293
- | PlanCommand
294
- | PrescribeCommand
295
- | QuestionnaireCommand
296
- | ReasonForVisitCommand
297
- | StopMedicationCommand
298
- | UpdateGoalCommand
299
- ),
258
+ Command: _BaseCommand,
300
259
  fields_to_test: tuple[str],
301
260
  ) -> None:
302
261
  schema = Command.model_json_schema()
@@ -323,136 +282,3 @@ def test_command_allows_kwarg_with_correct_type(
323
282
  cmd = Command(**base)
324
283
  effect = getattr(cmd, method)()
325
284
  assert effect is not None
326
-
327
-
328
- @pytest.fixture(scope="session")
329
- def token() -> MaskedValue:
330
- return MaskedValue(
331
- requests.post(
332
- f"{settings.INTEGRATION_TEST_URL}/auth/token/",
333
- headers={"Content-Type": "application/x-www-form-urlencoded"},
334
- data={
335
- "grant_type": "client_credentials",
336
- "client_id": settings.INTEGRATION_TEST_CLIENT_ID,
337
- "client_secret": settings.INTEGRATION_TEST_CLIENT_SECRET,
338
- },
339
- ).json()["access_token"]
340
- )
341
-
342
-
343
- @pytest.fixture
344
- def note_uuid(token: MaskedValue) -> str:
345
- headers = {
346
- "Authorization": f"Bearer {token.value}",
347
- "Content-Type": "application/json",
348
- "Accept": "application/json",
349
- }
350
- data = {
351
- "patient": 1,
352
- "provider": 1,
353
- "note_type": "office",
354
- "note_type_version": 1,
355
- "lastModifiedBySessionKey": "8fee3c03a525cebee1d8a6b8e63dd4dg",
356
- }
357
- note = requests.post(
358
- f"{settings.INTEGRATION_TEST_URL}/api/Note/", headers=headers, json=data
359
- ).json()
360
- return note["externallyExposableId"]
361
-
362
-
363
- @pytest.fixture
364
- def command_type_map() -> dict[str, type]:
365
- return {
366
- "AutocompleteField": str,
367
- "MultiLineTextField": str,
368
- "TextField": str,
369
- "ChoiceField": str,
370
- "DateField": datetime,
371
- "ApproximateDateField": datetime,
372
- "IntegerField": int,
373
- "DecimalField": decimal.Decimal,
374
- }
375
-
376
-
377
- @pytest.mark.integtest
378
- @pytest.mark.parametrize(
379
- "Command",
380
- [
381
- (AssessCommand),
382
- (DiagnoseCommand),
383
- (GoalCommand),
384
- (HistoryOfPresentIllnessCommand),
385
- (MedicationStatementCommand),
386
- (PlanCommand),
387
- (PrescribeCommand),
388
- (QuestionnaireCommand),
389
- (ReasonForVisitCommand),
390
- (StopMedicationCommand),
391
- (UpdateGoalCommand),
392
- ],
393
- )
394
- def test_command_schema_matches_command_api(
395
- token: MaskedValue,
396
- command_type_map: dict[str, str],
397
- note_uuid: str,
398
- Command: (
399
- AssessCommand
400
- | DiagnoseCommand
401
- | GoalCommand
402
- | HistoryOfPresentIllnessCommand
403
- | MedicationStatementCommand
404
- | PlanCommand
405
- | PrescribeCommand
406
- | QuestionnaireCommand
407
- | ReasonForVisitCommand
408
- | StopMedicationCommand
409
- | UpdateGoalCommand
410
- ),
411
- ) -> None:
412
- # first create the command in the new note
413
- data = {"noteKey": note_uuid, "schemaKey": Command.Meta.key}
414
- headers = {"Authorization": f"Bearer {token.value}"}
415
- url = f"{settings.INTEGRATION_TEST_URL}/core/api/v1/commands/"
416
- command_resp = requests.post(url, headers=headers, data=data).json()
417
- assert "uuid" in command_resp
418
- command_uuid = command_resp["uuid"]
419
-
420
- # next, request the fields of the newly created command
421
- url = f"{settings.INTEGRATION_TEST_URL}/core/api/v1/commands/{command_uuid}/fields/"
422
- command_fields_resp = requests.get(url, headers=headers).json()
423
- assert command_fields_resp["schema"] == Command.Meta.key
424
-
425
- command_fields = command_fields_resp["fields"]
426
- if Command.Meta.key == "questionnaire":
427
- # questionnaire's fields vary per questionnaire, so just check the first two fields which never vary
428
- command_fields = command_fields[:2]
429
- expected_fields = Command.command_schema()
430
- assert len(command_fields) == len(expected_fields)
431
-
432
- for actual_field in command_fields:
433
- name = actual_field["name"]
434
- assert name in expected_fields
435
- expected_field = expected_fields[name]
436
-
437
- assert expected_field["required"] == actual_field["required"]
438
-
439
- expected_type = expected_field["type"]
440
- if expected_type is Coding:
441
- expected_type = expected_type.__annotations__["code"]
442
-
443
- actual_type = command_type_map.get(actual_field["type"])
444
- if actual_field["type"] == "AutocompleteField" and name[-1] == "s":
445
- # this condition initially created for Prescribe.indications,
446
- # but could apply to other AutocompleteField fields that are lists
447
- # making the assumption here that if the field ends in 's' (like indications), it is a list
448
- actual_type = list[actual_type] # type: ignore
449
-
450
- assert expected_type == actual_type
451
-
452
- if (choices := actual_field["choices"]) is None:
453
- assert expected_field["choices"] is None
454
- continue
455
-
456
- assert len(expected_field["choices"]) == len(choices)
457
- for choice in choices:
458
- assert choice["value"] in expected_field["choices"]
@@ -13,6 +13,7 @@ class AddBannerAlert(_BaseEffect):
13
13
 
14
14
  class Meta:
15
15
  effect_type = EffectType.ADD_BANNER_ALERT
16
+ apply_required_fields = ("patient_id", "key", "narrative", "placement", "intent")
16
17
 
17
18
  class Placement(Enum):
18
19
  CHART = "chart"
@@ -26,11 +27,11 @@ class AddBannerAlert(_BaseEffect):
26
27
  WARNING = "warning"
27
28
  ALERT = "alert"
28
29
 
29
- patient_id: str
30
- key: str
31
- narrative: str = Field(max_length=90)
32
- placement: list[Placement] = Field(min_length=1)
33
- intent: Intent
30
+ patient_id: str | None = None
31
+ key: str | None = None
32
+ narrative: str | None = Field(max_length=90, default=None)
33
+ placement: list[Placement] | None = Field(min_length=1, default=None)
34
+ intent: Intent | None = None
34
35
  href: str | None = None
35
36
 
36
37
  @property
@@ -38,8 +39,8 @@ class AddBannerAlert(_BaseEffect):
38
39
  """The BannerAlert's values."""
39
40
  return {
40
41
  "narrative": self.narrative,
41
- "placement": [p.value for p in self.placement],
42
- "intent": self.intent.value,
42
+ "placement": [p.value for p in self.placement] if self.placement else None,
43
+ "intent": self.intent.value if self.intent else None,
43
44
  "href": self.href,
44
45
  }
45
46
 
@@ -10,9 +10,10 @@ class RemoveBannerAlert(_BaseEffect):
10
10
 
11
11
  class Meta:
12
12
  effect_type = EffectType.REMOVE_BANNER_ALERT
13
+ apply_required_fields = ("patient_id", "key")
13
14
 
14
- patient_id: str
15
- key: str
15
+ patient_id: str | None = None
16
+ key: str | None = None
16
17
 
17
18
  @property
18
19
  def effect_payload(self) -> dict[str, Any]:
@@ -0,0 +1,224 @@
1
+ import shutil
2
+ from contextlib import chdir
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ from typing import Any, Generator
6
+
7
+ import pytest
8
+ import requests
9
+ from pydantic import ValidationError
10
+ from typer.testing import CliRunner
11
+
12
+ import settings
13
+ from canvas_cli.apps.plugin.plugin import _build_package, plugin_url
14
+ from canvas_cli.main import app
15
+ from canvas_sdk.commands.tests.test_utils import MaskedValue
16
+ from canvas_sdk.effects.banner_alert import AddBannerAlert, RemoveBannerAlert
17
+
18
+ runner = CliRunner()
19
+
20
+
21
+ @pytest.fixture(scope="session")
22
+ def token() -> MaskedValue:
23
+ return MaskedValue(
24
+ requests.post(
25
+ f"{settings.INTEGRATION_TEST_URL}/auth/token/",
26
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
27
+ data={
28
+ "grant_type": "client_credentials",
29
+ "client_id": settings.INTEGRATION_TEST_CLIENT_ID,
30
+ "client_secret": settings.INTEGRATION_TEST_CLIENT_SECRET,
31
+ },
32
+ ).json()["access_token"]
33
+ )
34
+
35
+
36
+ @pytest.fixture(scope="session")
37
+ def first_patient_id(token: MaskedValue) -> dict:
38
+ headers = {
39
+ "Authorization": f"Bearer {token.value}",
40
+ "Content-Type": "application/json",
41
+ "Accept": "application/json",
42
+ }
43
+ patients = requests.get(f"{settings.INTEGRATION_TEST_URL}/api/Patient", headers=headers).json()
44
+ return patients["entry"][0]["resource"]["key"]
45
+
46
+
47
+ @pytest.fixture(scope="session")
48
+ def plugin_name() -> str:
49
+ return f"addbanneralert{datetime.now().timestamp()}".replace(".", "")
50
+
51
+
52
+ @pytest.fixture(autouse=True, scope="session")
53
+ def write_and_install_protocol_and_clean_up(
54
+ first_patient_id: str, plugin_name: str, token: MaskedValue
55
+ ) -> Generator[Any, Any, Any]:
56
+ # write the protocol
57
+ with chdir(Path("./custom-plugins")):
58
+ runner.invoke(app, "init", input=plugin_name)
59
+
60
+ protocol = open(f"./custom-plugins/{plugin_name}/protocols/my_protocol.py", "w")
61
+ protocol.write(
62
+ f"""from canvas_sdk.effects.banner_alert import AddBannerAlert
63
+ from canvas_sdk.events import EventType
64
+ from canvas_sdk.protocols import BaseProtocol
65
+
66
+ class Protocol(BaseProtocol):
67
+ RESPONDS_TO = EventType.Name(EventType.ENCOUNTER_CREATED)
68
+ def compute(self):
69
+ return [
70
+ AddBannerAlert(
71
+ patient_id="{first_patient_id}",
72
+ key="{plugin_name}",
73
+ narrative="this is a test",
74
+ placement=[AddBannerAlert.Placement.CHART],
75
+ intent=AddBannerAlert.Intent.INFO,
76
+ ).apply()
77
+ ]
78
+ """
79
+ )
80
+ protocol.close()
81
+
82
+ # install the plugin
83
+ requests.post(
84
+ plugin_url(settings.INTEGRATION_TEST_URL),
85
+ data={"is_enabled": True},
86
+ files={"package": open(_build_package(Path(f"./custom-plugins/{plugin_name}")), "rb")},
87
+ headers={"Authorization": f"Bearer {token.value}"},
88
+ )
89
+
90
+ yield
91
+
92
+ # clean up
93
+ if Path(f"./custom-plugins/{plugin_name}").exists():
94
+ shutil.rmtree(Path(f"./custom-plugins/{plugin_name}"))
95
+
96
+ # disable
97
+ requests.patch(
98
+ plugin_url(settings.INTEGRATION_TEST_URL, plugin_name),
99
+ data={"is_enabled": False},
100
+ headers={
101
+ "Authorization": f"Bearer {token.value}",
102
+ },
103
+ )
104
+ # delete
105
+ requests.delete(
106
+ plugin_url(settings.INTEGRATION_TEST_URL, plugin_name),
107
+ headers={"Authorization": f"Bearer {token.value}"},
108
+ )
109
+
110
+ # confirm no more banner
111
+ patient_banners_none = requests.get(
112
+ f"{settings.INTEGRATION_TEST_URL}/api/BannerAlert/?patient__key={first_patient_id}",
113
+ headers={
114
+ "Authorization": f"Bearer {token.value}",
115
+ },
116
+ ).json()
117
+ patient_banner = next(
118
+ (b for b in patient_banners_none["results"] if b["key"] == plugin_name), None
119
+ )
120
+ assert patient_banner is None
121
+
122
+
123
+ @pytest.mark.integtest
124
+ def test_protocol_that_adds_banner_alert(
125
+ token: MaskedValue, plugin_name: str, first_patient_id: str
126
+ ) -> None:
127
+ # trigger the event
128
+ requests.post(
129
+ f"{settings.INTEGRATION_TEST_URL}/api/Note/",
130
+ headers={
131
+ "Authorization": f"Bearer {token.value}",
132
+ "Content-Type": "application/json",
133
+ "Accept": "application/json",
134
+ },
135
+ json={
136
+ "patient": 1,
137
+ "provider": 1,
138
+ "note_type": "office",
139
+ "note_type_version": 1,
140
+ "lastModifiedBySessionKey": "8fee3c03a525cebee1d8a6b8e63dd4dg",
141
+ },
142
+ )
143
+
144
+ patient_banners = requests.get(
145
+ f"{settings.INTEGRATION_TEST_URL}/api/BannerAlert/?patient__key={first_patient_id}",
146
+ headers={
147
+ "Authorization": f"Bearer {token.value}",
148
+ },
149
+ ).json()
150
+ assert patient_banners["count"] > 0
151
+
152
+ patient_banner = next(b for b in patient_banners["results"] if b["key"] == plugin_name)
153
+ assert patient_banner["pluginName"] == plugin_name
154
+ assert patient_banner["narrative"] == "this is a test"
155
+ assert patient_banner["placement"] == ["chart"]
156
+ assert patient_banner["intent"] == "info"
157
+ assert patient_banner["href"] is None
158
+ assert patient_banner["status"] == "active"
159
+
160
+
161
+ @pytest.mark.parametrize(
162
+ "Effect,params,expected_payload",
163
+ [
164
+ (
165
+ AddBannerAlert,
166
+ {
167
+ "patient_id": "uuid",
168
+ "key": "test-key",
169
+ "narrative": "hellooo",
170
+ "placement": [AddBannerAlert.Placement.APPOINTMENT_CARD],
171
+ "intent": AddBannerAlert.Intent.INFO,
172
+ },
173
+ '{"patient": "uuid", "key": "test-key", "data": {"narrative": "hellooo", "placement": ["appointment_card"], "intent": "info", "href": null}}',
174
+ ),
175
+ (
176
+ RemoveBannerAlert,
177
+ {"patient_id": "uuid", "key": "testeroo"},
178
+ '{"patient": "uuid", "key": "testeroo"}',
179
+ ),
180
+ ],
181
+ )
182
+ def test_banner_alert_apply_method_succeeds_with_all_required_fields(
183
+ Effect: AddBannerAlert | RemoveBannerAlert, params: dict, expected_payload: str
184
+ ) -> None:
185
+ b = Effect()
186
+ for k, v in params.items():
187
+ setattr(b, k, v)
188
+ applied = b.apply()
189
+ assert applied.payload == expected_payload
190
+
191
+
192
+ @pytest.mark.parametrize(
193
+ "Effect,expected_err_msgs",
194
+ [
195
+ (
196
+ AddBannerAlert,
197
+ [
198
+ "5 validation errors for AddBannerAlert",
199
+ "Field 'patient_id' is required to apply an AddBannerAlert [type=missing",
200
+ "Field 'key' is required to apply an AddBannerAlert [type=missing",
201
+ "Field 'narrative' is required to apply an AddBannerAlert [type=missing",
202
+ "Field 'placement' is required to apply an AddBannerAlert [type=missing",
203
+ "Field 'intent' is required to apply an AddBannerAlert [type=missing",
204
+ ],
205
+ ),
206
+ (
207
+ RemoveBannerAlert,
208
+ [
209
+ "2 validation errors for RemoveBannerAlert",
210
+ "Field 'patient_id' is required to apply a RemoveBannerAlert [type=missing",
211
+ "Field 'key' is required to apply a RemoveBannerAlert [type=missing",
212
+ ],
213
+ ),
214
+ ],
215
+ )
216
+ def test_banner_alert_apply_method_raises_error_without_required_fields(
217
+ Effect: AddBannerAlert | RemoveBannerAlert, expected_err_msgs: str
218
+ ) -> None:
219
+ b = Effect()
220
+ with pytest.raises(ValidationError) as e:
221
+ b.apply()
222
+ err_msg = repr(e.value)
223
+ for expected in expected_err_msgs:
224
+ assert expected in err_msg
@@ -1,12 +1,11 @@
1
1
  import json
2
2
  from typing import Any
3
3
 
4
- from pydantic import BaseModel, ConfigDict
5
-
4
+ from canvas_sdk.base import Model
6
5
  from canvas_sdk.effects import Effect, EffectType
7
6
 
8
7
 
9
- class _BaseEffect(BaseModel):
8
+ class _BaseEffect(Model):
10
9
  """
11
10
  A Canvas Effect that changes user behavior or autonomously performs activities on behalf of users.
12
11
  """
@@ -14,8 +13,6 @@ class _BaseEffect(BaseModel):
14
13
  class Meta:
15
14
  effect_type = EffectType.UNKNOWN_EFFECT
16
15
 
17
- model_config = ConfigDict(strict=True, validate_assignment=True)
18
-
19
16
  @property
20
17
  def values(self) -> dict[str, Any]:
21
18
  return {}
@@ -25,4 +22,5 @@ class _BaseEffect(BaseModel):
25
22
  return {"data": self.values}
26
23
 
27
24
  def apply(self) -> Effect:
25
+ self._validate_before_effect("apply")
28
26
  return Effect(type=self.Meta.effect_type, payload=json.dumps(self.effect_payload))
@@ -0,0 +1,39 @@
1
+ from enum import Enum
2
+ from typing import Any
3
+
4
+ from pydantic import Field
5
+
6
+ from canvas_sdk.effects.base import EffectType, _BaseEffect
7
+
8
+
9
+ class PatientChartSummaryConfiguration(_BaseEffect):
10
+ """
11
+ An Effect that will decide which sections appear on the patient's chart summary in Canvas.
12
+ """
13
+
14
+ class Meta:
15
+ effect_type = EffectType.SHOW_PATIENT_CHART_SUMMARY_SECTIONS
16
+
17
+ class Section(Enum):
18
+ SOCIAL_DETERMINANTS = "social_determinants"
19
+ GOALS = "goals"
20
+ CONDITIONS = "conditions"
21
+ MEDICATIONS = "medications"
22
+ ALLERGIES = "allergies"
23
+ CARE_TEAMS = "care_teams"
24
+ VITALS = "vitals"
25
+ IMMUNIZATIONS = "immunizations"
26
+ SURGICAL_HISTORY = "surgical_history"
27
+ FAMILY_HISTORY = "family_history"
28
+
29
+ sections: list[Section] = Field(min_length=1)
30
+
31
+ @property
32
+ def values(self) -> dict[str, Any]:
33
+ """The PatientChartSummaryConfiguration's values."""
34
+ return {"sections": [s.value for s in self.sections]}
35
+
36
+ @property
37
+ def effect_payload(self) -> dict[str, Any]:
38
+ """The payload of the effect."""
39
+ return {"data": self.values}
@@ -0,0 +1 @@
1
+ from canvas_sdk.effects.protocol_card.protocol_card import ProtocolCard, Recommendation