canvas 0.1.3__py3-none-any.whl → 0.1.5__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 (70) hide show
  1. canvas-0.1.5.dist-info/METADATA +176 -0
  2. canvas-0.1.5.dist-info/RECORD +66 -0
  3. {canvas-0.1.3.dist-info → canvas-0.1.5.dist-info}/WHEEL +1 -1
  4. canvas-0.1.5.dist-info/entry_points.txt +3 -0
  5. canvas_cli/apps/__init__.py +0 -0
  6. canvas_cli/apps/auth/__init__.py +3 -0
  7. canvas_cli/apps/auth/tests.py +142 -0
  8. canvas_cli/apps/auth/utils.py +163 -0
  9. canvas_cli/apps/logs/__init__.py +3 -0
  10. canvas_cli/apps/logs/logs.py +59 -0
  11. canvas_cli/apps/plugin/__init__.py +9 -0
  12. canvas_cli/apps/plugin/plugin.py +286 -0
  13. canvas_cli/apps/plugin/tests.py +32 -0
  14. canvas_cli/conftest.py +28 -0
  15. canvas_cli/main.py +78 -0
  16. canvas_cli/templates/plugins/default/cookiecutter.json +4 -0
  17. canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/CANVAS_MANIFEST.json +29 -0
  18. canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/README.md +12 -0
  19. canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/protocols/__init__.py +0 -0
  20. canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/protocols/my_protocol.py +55 -0
  21. canvas_cli/tests.py +11 -0
  22. canvas_cli/utils/__init__.py +0 -0
  23. canvas_cli/utils/context/__init__.py +3 -0
  24. canvas_cli/utils/context/context.py +172 -0
  25. canvas_cli/utils/context/tests.py +130 -0
  26. canvas_cli/utils/print/__init__.py +3 -0
  27. canvas_cli/utils/print/print.py +60 -0
  28. canvas_cli/utils/print/tests.py +70 -0
  29. canvas_cli/utils/urls/__init__.py +3 -0
  30. canvas_cli/utils/urls/tests.py +12 -0
  31. canvas_cli/utils/urls/urls.py +27 -0
  32. canvas_cli/utils/validators/__init__.py +3 -0
  33. canvas_cli/utils/validators/manifest_schema.py +80 -0
  34. canvas_cli/utils/validators/tests.py +36 -0
  35. canvas_cli/utils/validators/validators.py +40 -0
  36. canvas_sdk/__init__.py +0 -0
  37. canvas_sdk/commands/__init__.py +27 -0
  38. canvas_sdk/commands/base.py +118 -0
  39. canvas_sdk/commands/commands/assess.py +48 -0
  40. canvas_sdk/commands/commands/diagnose.py +44 -0
  41. canvas_sdk/commands/commands/goal.py +48 -0
  42. canvas_sdk/commands/commands/history_present_illness.py +15 -0
  43. canvas_sdk/commands/commands/medication_statement.py +28 -0
  44. canvas_sdk/commands/commands/plan.py +15 -0
  45. canvas_sdk/commands/commands/prescribe.py +48 -0
  46. canvas_sdk/commands/commands/questionnaire.py +17 -0
  47. canvas_sdk/commands/commands/reason_for_visit.py +36 -0
  48. canvas_sdk/commands/commands/stop_medication.py +18 -0
  49. canvas_sdk/commands/commands/update_goal.py +48 -0
  50. canvas_sdk/commands/constants.py +9 -0
  51. canvas_sdk/commands/tests/test_utils.py +195 -0
  52. canvas_sdk/commands/tests/tests.py +407 -0
  53. canvas_sdk/data/__init__.py +0 -0
  54. canvas_sdk/effects/__init__.py +1 -0
  55. canvas_sdk/effects/banner_alert/banner_alert.py +37 -0
  56. canvas_sdk/effects/banner_alert/constants.py +19 -0
  57. canvas_sdk/effects/base.py +30 -0
  58. canvas_sdk/events/__init__.py +1 -0
  59. canvas_sdk/protocols/__init__.py +1 -0
  60. canvas_sdk/protocols/base.py +12 -0
  61. canvas_sdk/tests/__init__.py +0 -0
  62. canvas_sdk/utils/__init__.py +3 -0
  63. canvas_sdk/utils/http.py +72 -0
  64. canvas_sdk/utils/tests.py +63 -0
  65. canvas_sdk/views/__init__.py +0 -0
  66. canvas/main.py +0 -19
  67. canvas-0.1.3.dist-info/METADATA +0 -285
  68. canvas-0.1.3.dist-info/RECORD +0 -6
  69. canvas-0.1.3.dist-info/entry_points.txt +0 -3
  70. {canvas → canvas_cli}/__init__.py +0 -0
@@ -0,0 +1,195 @@
1
+ import random
2
+ import string
3
+ from datetime import datetime
4
+ from decimal import Decimal
5
+ from typing import Any
6
+
7
+ import pytest
8
+ from pydantic import ValidationError
9
+
10
+ from canvas_sdk.commands import (
11
+ AssessCommand,
12
+ DiagnoseCommand,
13
+ GoalCommand,
14
+ HistoryOfPresentIllnessCommand,
15
+ MedicationStatementCommand,
16
+ PlanCommand,
17
+ PrescribeCommand,
18
+ QuestionnaireCommand,
19
+ ReasonForVisitCommand,
20
+ StopMedicationCommand,
21
+ )
22
+ from canvas_sdk.commands.constants import Coding
23
+
24
+
25
+ def get_field_type_unformatted(field_props: dict[str, Any]) -> str:
26
+ if t := field_props.get("type"):
27
+ return field_props.get("format") or t
28
+
29
+ first_in_union: dict = field_props.get("anyOf", field_props.get("allOf"))[0]
30
+ if "$ref" in first_in_union:
31
+ return first_in_union["$ref"].split("#/$defs/")[-1]
32
+ return first_in_union.get("format") or first_in_union["type"]
33
+
34
+
35
+ def get_field_type(field_props: dict) -> str:
36
+ return get_field_type_unformatted(field_props).replace("-", "").replace("array", "list")
37
+
38
+
39
+ def random_string() -> str:
40
+ return "".join(random.choices(string.ascii_uppercase + string.digits, k=7))
41
+
42
+
43
+ def fake(
44
+ field_props: dict,
45
+ Command: (
46
+ AssessCommand
47
+ | DiagnoseCommand
48
+ | GoalCommand
49
+ | HistoryOfPresentIllnessCommand
50
+ | MedicationStatementCommand
51
+ | PlanCommand
52
+ | PrescribeCommand
53
+ | QuestionnaireCommand
54
+ | ReasonForVisitCommand
55
+ | StopMedicationCommand
56
+ ),
57
+ ) -> Any:
58
+ t = get_field_type(field_props)
59
+ match t:
60
+ case "string":
61
+ return random_string()
62
+ case "integer":
63
+ return random.randint(1, 10)
64
+ case "datetime":
65
+ return datetime.now()
66
+ case "boolean":
67
+ return random.choice([True, False])
68
+ case "number":
69
+ return Decimal(random.randrange(1, 200))
70
+ case "array":
71
+ num_items = random.randint(0, 5)
72
+ item_props = field_props["anyOf"][0]["items"]
73
+ return [fake(item_props, Command) for i in range(num_items)]
74
+ case "Coding":
75
+ return Coding(system=random_string(), code=random_string(), display=random_string())
76
+ if t[0].isupper():
77
+ return random.choice([e for e in getattr(Command, t)])
78
+
79
+
80
+ def raises_missing_error(
81
+ base: dict,
82
+ Command: (
83
+ AssessCommand
84
+ | DiagnoseCommand
85
+ | GoalCommand
86
+ | HistoryOfPresentIllnessCommand
87
+ | MedicationStatementCommand
88
+ | PlanCommand
89
+ | PrescribeCommand
90
+ | QuestionnaireCommand
91
+ | ReasonForVisitCommand
92
+ | StopMedicationCommand
93
+ ),
94
+ field: str,
95
+ ) -> None:
96
+ err_kwargs = base.copy()
97
+ err_kwargs.pop(field)
98
+ with pytest.raises(ValidationError) as e:
99
+ Command(**err_kwargs)
100
+ err_msg = repr(e.value)
101
+ assert (
102
+ f"1 validation error for {Command.__name__}\n{field}\n Field required [type=missing"
103
+ in err_msg
104
+ )
105
+
106
+
107
+ def raises_none_error(
108
+ base: dict,
109
+ Command: (
110
+ AssessCommand
111
+ | DiagnoseCommand
112
+ | GoalCommand
113
+ | HistoryOfPresentIllnessCommand
114
+ | MedicationStatementCommand
115
+ | PlanCommand
116
+ | PrescribeCommand
117
+ | QuestionnaireCommand
118
+ | ReasonForVisitCommand
119
+ | StopMedicationCommand
120
+ ),
121
+ field: str,
122
+ ) -> None:
123
+ field_props = Command.model_json_schema()["properties"][field]
124
+ field_type = get_field_type(field_props)
125
+
126
+ with pytest.raises(ValidationError) as e1:
127
+ err_kwargs = base | {field: None}
128
+ Command(**err_kwargs)
129
+ err_msg1 = repr(e1.value)
130
+
131
+ valid_kwargs = base | {field: fake(field_props, Command)}
132
+ cmd = Command(**valid_kwargs)
133
+ with pytest.raises(ValidationError) as e2:
134
+ setattr(cmd, field, None)
135
+ err_msg2 = repr(e2.value)
136
+
137
+ assert f"1 validation error for {Command.__name__}\n{field}" in err_msg1
138
+ assert f"1 validation error for {Command.__name__}\n{field}" in err_msg2
139
+
140
+ if field_type == "number":
141
+ assert f"Input should be an instance of Decimal" in err_msg1
142
+ assert f"Input should be an instance of Decimal" in err_msg2
143
+ elif field_type[0].isupper():
144
+ assert f"Input should be an instance of {Command.__name__}.{field_type}" in err_msg1
145
+ assert f"Input should be an instance of {Command.__name__}.{field_type}" in err_msg2
146
+ else:
147
+ assert f"Input should be a valid {field_type}" in err_msg1
148
+ assert f"Input should be a valid {field_type}" in err_msg2
149
+
150
+
151
+ def raises_wrong_type_error(
152
+ base: dict,
153
+ Command: (
154
+ AssessCommand
155
+ | DiagnoseCommand
156
+ | GoalCommand
157
+ | HistoryOfPresentIllnessCommand
158
+ | MedicationStatementCommand
159
+ | PlanCommand
160
+ | PrescribeCommand
161
+ | QuestionnaireCommand
162
+ | ReasonForVisitCommand
163
+ | StopMedicationCommand
164
+ ),
165
+ field: str,
166
+ ) -> None:
167
+ field_props = Command.model_json_schema()["properties"][field]
168
+ field_type = get_field_type(field_props)
169
+ wrong_field_type = "integer" if field_type == "string" else "string"
170
+
171
+ with pytest.raises(ValidationError) as e1:
172
+ err_kwargs = base | {field: fake({"type": wrong_field_type}, Command)}
173
+ Command(**err_kwargs)
174
+ err_msg1 = repr(e1.value)
175
+
176
+ valid_kwargs = base | {field: fake(field_props, Command)}
177
+ cmd = Command(**valid_kwargs)
178
+ err_value = fake({"type": wrong_field_type}, Command)
179
+ with pytest.raises(ValidationError) as e2:
180
+ setattr(cmd, field, err_value)
181
+ err_msg2 = repr(e2.value)
182
+
183
+ assert f"1 validation error for {Command.__name__}\n{field}" in err_msg1
184
+ assert f"1 validation error for {Command.__name__}\n{field}" in err_msg2
185
+
186
+ field_type = "dictionary" if field_type == "Coding" else field_type
187
+ if field_type == "number":
188
+ assert f"Input should be an instance of Decimal" in err_msg1
189
+ assert f"Input should be an instance of Decimal" in err_msg2
190
+ elif field_type[0].isupper():
191
+ assert f"Input should be an instance of {Command.__name__}.{field_type}" in err_msg1
192
+ assert f"Input should be an instance of {Command.__name__}.{field_type}" in err_msg2
193
+ else:
194
+ assert f"Input should be a valid {field_type}" in err_msg1
195
+ assert f"Input should be a valid {field_type}" in err_msg2
@@ -0,0 +1,407 @@
1
+ from datetime import datetime
2
+
3
+ import pytest
4
+ import requests
5
+ from pydantic import ValidationError
6
+
7
+ import settings
8
+ from canvas_sdk.commands import (
9
+ AssessCommand,
10
+ DiagnoseCommand,
11
+ GoalCommand,
12
+ HistoryOfPresentIllnessCommand,
13
+ MedicationStatementCommand,
14
+ PlanCommand,
15
+ PrescribeCommand,
16
+ QuestionnaireCommand,
17
+ ReasonForVisitCommand,
18
+ StopMedicationCommand,
19
+ UpdateGoalCommand,
20
+ )
21
+ from canvas_sdk.commands.constants import Coding
22
+ from canvas_sdk.commands.tests.test_utils import (
23
+ fake,
24
+ get_field_type,
25
+ raises_missing_error,
26
+ raises_none_error,
27
+ raises_wrong_type_error,
28
+ )
29
+
30
+
31
+ @pytest.mark.parametrize(
32
+ "Command,fields_to_test",
33
+ [
34
+ (AssessCommand, ("condition_id", "background", "status", "narrative")),
35
+ (
36
+ DiagnoseCommand,
37
+ ("icd10_code", "background", "approximate_date_of_onset", "today_assessment"),
38
+ ),
39
+ (
40
+ GoalCommand,
41
+ (
42
+ "goal_statement",
43
+ "start_date",
44
+ "due_date",
45
+ "achievement_status",
46
+ "priority",
47
+ "progress",
48
+ ),
49
+ ),
50
+ (HistoryOfPresentIllnessCommand, ("narrative",)),
51
+ (MedicationStatementCommand, ("fdb_code", "sig")),
52
+ (PlanCommand, ("narrative", "user_id", "command_uuid")),
53
+ (
54
+ PrescribeCommand,
55
+ (
56
+ "fdb_code",
57
+ "icd10_codes",
58
+ "sig",
59
+ "days_supply",
60
+ "quantity_to_dispense",
61
+ "type_to_dispense",
62
+ "refills",
63
+ "substitutions",
64
+ "pharmacy",
65
+ "prescriber_id",
66
+ "note_to_pharmacist",
67
+ ),
68
+ ),
69
+ (QuestionnaireCommand, ("questionnaire_id", "result")),
70
+ (ReasonForVisitCommand, ("coding", "comment")),
71
+ (StopMedicationCommand, ("medication_id", "rationale")),
72
+ (
73
+ UpdateGoalCommand,
74
+ (
75
+ "goal_id",
76
+ "due_date",
77
+ "achievement_status",
78
+ "priority",
79
+ "progress",
80
+ ),
81
+ ),
82
+ ],
83
+ )
84
+ 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
+ ),
98
+ fields_to_test: tuple[str],
99
+ ) -> None:
100
+ schema = Command.model_json_schema()
101
+ schema["required"].append("note_id")
102
+ required_fields = {k: v for k, v in schema["properties"].items() if k in schema["required"]}
103
+ base = {field: fake(props, Command) for field, props in required_fields.items()}
104
+ for field in fields_to_test:
105
+ raises_wrong_type_error(base, Command, field)
106
+ if field in required_fields:
107
+ raises_missing_error(base, Command, field)
108
+ raises_none_error(base, Command, field)
109
+
110
+
111
+ @pytest.mark.parametrize(
112
+ "Command,err_kwargs,err_msg,valid_kwargs",
113
+ [
114
+ (
115
+ PlanCommand,
116
+ {"narrative": "yo", "user_id": 1},
117
+ "1 validation error for PlanCommand\n Value error, Command should have either a note_id or a command_uuid. [type=value",
118
+ {"narrative": "yo", "note_id": 1, "user_id": 1},
119
+ ),
120
+ (
121
+ PlanCommand,
122
+ {"narrative": "yo", "user_id": 1, "note_id": None},
123
+ "1 validation error for PlanCommand\n Value error, Command should have either a note_id or a command_uuid. [type=value",
124
+ {"narrative": "yo", "note_id": 1, "user_id": 1},
125
+ ),
126
+ (
127
+ PlanCommand,
128
+ {"narrative": "yo", "user_id": 5, "note_id": "100"},
129
+ "1 validation error for PlanCommand\nnote_id\n Input should be a valid integer [type=int_type",
130
+ {"narrative": "yo", "note_id": 1, "user_id": 1},
131
+ ),
132
+ (
133
+ ReasonForVisitCommand,
134
+ {"note_id": 1, "user_id": 1, "structured": True},
135
+ "1 validation error for ReasonForVisitCommand\n Value error, Structured RFV should have a coding.",
136
+ {"note_id": 1, "user_id": 1, "structured": False},
137
+ ),
138
+ (
139
+ ReasonForVisitCommand,
140
+ {"note_id": 1, "user_id": 1, "coding": {"code": "x"}},
141
+ "1 validation error for ReasonForVisitCommand\ncoding.system\n Field required [type=missing",
142
+ {"note_id": 1, "user_id": 1},
143
+ ),
144
+ (
145
+ ReasonForVisitCommand,
146
+ {"note_id": 1, "user_id": 1, "coding": {"code": 1, "system": "y"}},
147
+ "1 validation error for ReasonForVisitCommand\ncoding.code\n Input should be a valid string [type=string_type",
148
+ {"note_id": 1, "user_id": 1},
149
+ ),
150
+ (
151
+ ReasonForVisitCommand,
152
+ {"note_id": 1, "user_id": 1, "coding": {"code": None, "system": "y"}},
153
+ "1 validation error for ReasonForVisitCommand\ncoding.code\n Input should be a valid string [type=string_type",
154
+ {"note_id": 1, "user_id": 1},
155
+ ),
156
+ (
157
+ ReasonForVisitCommand,
158
+ {"note_id": 1, "user_id": 1, "coding": {"system": "y"}},
159
+ "1 validation error for ReasonForVisitCommand\ncoding.code\n Field required [type=missing",
160
+ {"note_id": 1, "user_id": 1},
161
+ ),
162
+ (
163
+ ReasonForVisitCommand,
164
+ {"note_id": 1, "user_id": 1, "coding": {"code": "x", "system": 1}},
165
+ "1 validation error for ReasonForVisitCommand\ncoding.system\n Input should be a valid string [type=string_type",
166
+ {"note_id": 1, "user_id": 1},
167
+ ),
168
+ (
169
+ ReasonForVisitCommand,
170
+ {"note_id": 1, "user_id": 1, "coding": {"code": "x", "system": None}},
171
+ "1 validation error for ReasonForVisitCommand\ncoding.system\n Input should be a valid string [type=string_type",
172
+ {"note_id": 1, "user_id": 1},
173
+ ),
174
+ (
175
+ ReasonForVisitCommand,
176
+ {"note_id": 1, "user_id": 1, "coding": {"code": "x", "system": "y", "display": 1}},
177
+ "1 validation error for ReasonForVisitCommand\ncoding.display\n Input should be a valid string [type=string_type",
178
+ {"note_id": 1, "user_id": 1},
179
+ ),
180
+ ],
181
+ )
182
+ def test_command_raises_specific_error_when_kwarg_given_incorrect_type(
183
+ Command: PlanCommand | ReasonForVisitCommand,
184
+ err_kwargs: dict,
185
+ err_msg: str,
186
+ valid_kwargs: dict,
187
+ ) -> None:
188
+ with pytest.raises(ValidationError) as e1:
189
+ Command(**err_kwargs)
190
+ assert err_msg in repr(e1.value)
191
+
192
+ cmd = Command(**valid_kwargs)
193
+ if len(err_kwargs) < len(valid_kwargs):
194
+ return
195
+ key, value = list(err_kwargs.items())[-1]
196
+ with pytest.raises(ValidationError) as e2:
197
+ setattr(cmd, key, value)
198
+ assert err_msg in repr(e2.value)
199
+
200
+
201
+ @pytest.mark.parametrize(
202
+ "Command,fields_to_test",
203
+ [
204
+ (AssessCommand, ("condition_id", "background", "status", "narrative")),
205
+ (
206
+ DiagnoseCommand,
207
+ ("icd10_code", "background", "approximate_date_of_onset", "today_assessment"),
208
+ ),
209
+ (
210
+ GoalCommand,
211
+ (
212
+ "goal_statement",
213
+ "start_date",
214
+ "due_date",
215
+ "achievement_status",
216
+ "priority",
217
+ "progress",
218
+ ),
219
+ ),
220
+ (HistoryOfPresentIllnessCommand, ("narrative",)),
221
+ (MedicationStatementCommand, ("fdb_code", "sig")),
222
+ (PlanCommand, ("narrative", "user_id", "command_uuid", "note_id")),
223
+ (
224
+ PrescribeCommand,
225
+ (
226
+ "fdb_code",
227
+ "icd10_codes",
228
+ "sig",
229
+ "days_supply",
230
+ "quantity_to_dispense",
231
+ "type_to_dispense",
232
+ "refills",
233
+ "substitutions",
234
+ "pharmacy",
235
+ "prescriber_id",
236
+ "note_to_pharmacist",
237
+ ),
238
+ ),
239
+ (QuestionnaireCommand, ("questionnaire_id", "result")),
240
+ (ReasonForVisitCommand, ("coding", "comment")),
241
+ (StopMedicationCommand, ("medication_id", "rationale")),
242
+ (
243
+ UpdateGoalCommand,
244
+ (
245
+ "goal_id",
246
+ "due_date",
247
+ "achievement_status",
248
+ "priority",
249
+ "progress",
250
+ ),
251
+ ),
252
+ ],
253
+ )
254
+ def test_command_allows_kwarg_with_correct_type(
255
+ Command: (
256
+ AssessCommand
257
+ | DiagnoseCommand
258
+ | GoalCommand
259
+ | HistoryOfPresentIllnessCommand
260
+ | MedicationStatementCommand
261
+ | PlanCommand
262
+ | PrescribeCommand
263
+ | QuestionnaireCommand
264
+ | ReasonForVisitCommand
265
+ | StopMedicationCommand
266
+ | UpdateGoalCommand
267
+ ),
268
+ fields_to_test: tuple[str],
269
+ ) -> None:
270
+ schema = Command.model_json_schema()
271
+ schema["required"].append("note_id")
272
+ required_fields = {k: v for k, v in schema["properties"].items() if k in schema["required"]}
273
+ base = {field: fake(props, Command) for field, props in required_fields.items()}
274
+
275
+ for field in fields_to_test:
276
+ field_type = get_field_type(Command.model_json_schema()["properties"][field])
277
+
278
+ init_field_value = fake({"type": field_type}, Command)
279
+ init_kwargs = base | {field: init_field_value}
280
+ cmd = Command(**init_kwargs)
281
+ assert getattr(cmd, field) == init_field_value
282
+
283
+ updated_field_value = fake({"type": field_type}, Command)
284
+ setattr(cmd, field, updated_field_value)
285
+ assert getattr(cmd, field) == updated_field_value
286
+
287
+
288
+ @pytest.fixture(scope="session")
289
+ def token() -> str:
290
+ return requests.post(
291
+ f"{settings.INTEGRATION_TEST_URL}/auth/token/",
292
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
293
+ data={
294
+ "grant_type": "client_credentials",
295
+ "client_id": settings.INTEGRATION_TEST_CLIENT_ID,
296
+ "client_secret": settings.INTEGRATION_TEST_CLIENT_SECRET,
297
+ },
298
+ ).json()["access_token"]
299
+
300
+
301
+ @pytest.fixture
302
+ def note_id(token: str) -> str:
303
+ headers = {
304
+ "Authorization": f"Bearer {token}",
305
+ "Content-Type": "application/json",
306
+ "Accept": "application/json",
307
+ }
308
+ data = {
309
+ "patient": 1,
310
+ "provider": 1,
311
+ "note_type": "office",
312
+ "note_type_version": 1,
313
+ "lastModifiedBySessionKey": "8fee3c03a525cebee1d8a6b8e63dd4dg",
314
+ }
315
+ note = requests.post(
316
+ f"{settings.INTEGRATION_TEST_URL}/api/Note/", headers=headers, json=data
317
+ ).json()
318
+ return note["externallyExposableId"]
319
+
320
+
321
+ @pytest.fixture
322
+ def command_type_map() -> dict[str, type]:
323
+ return {
324
+ "AutocompleteField": str,
325
+ "MultiLineTextField": str,
326
+ "TextField": str,
327
+ "ChoiceField": str,
328
+ "DateField": datetime,
329
+ }
330
+
331
+
332
+ @pytest.mark.integtest
333
+ @pytest.mark.parametrize(
334
+ "Command",
335
+ [
336
+ (AssessCommand),
337
+ # todo: add Diagnose once it has an adapter in home-app
338
+ # (DiagnoseCommand),
339
+ (GoalCommand),
340
+ (HistoryOfPresentIllnessCommand),
341
+ (MedicationStatementCommand),
342
+ (PlanCommand),
343
+ # todo: add Prescribe once its been refactored
344
+ # (PrescribeCommand),
345
+ (QuestionnaireCommand),
346
+ (ReasonForVisitCommand),
347
+ (StopMedicationCommand),
348
+ (UpdateGoalCommand),
349
+ ],
350
+ )
351
+ def test_command_schema_matches_command_api(
352
+ token: str,
353
+ command_type_map: dict[str, str],
354
+ note_id: str,
355
+ Command: (
356
+ AssessCommand
357
+ | DiagnoseCommand
358
+ | GoalCommand
359
+ | HistoryOfPresentIllnessCommand
360
+ | MedicationStatementCommand
361
+ | PlanCommand
362
+ | PrescribeCommand
363
+ | QuestionnaireCommand
364
+ | ReasonForVisitCommand
365
+ | StopMedicationCommand
366
+ | UpdateGoalCommand
367
+ ),
368
+ ) -> None:
369
+ # first create the command in the new note
370
+ data = {"noteKey": note_id, "schemaKey": Command.Meta.key}
371
+ headers = {"Authorization": f"Bearer {token}"}
372
+ url = f"{settings.INTEGRATION_TEST_URL}/core/api/v1/commands/"
373
+ command_resp = requests.post(url, headers=headers, data=data).json()
374
+ assert "uuid" in command_resp
375
+ command_uuid = command_resp["uuid"]
376
+
377
+ # next, request the fields of the newly created command
378
+ url = f"{settings.INTEGRATION_TEST_URL}/core/api/v1/commands/{command_uuid}/fields/"
379
+ command_fields_resp = requests.get(url, headers=headers).json()
380
+ assert command_fields_resp["schema"] == Command.Meta.key
381
+
382
+ command_fields = command_fields_resp["fields"]
383
+ if Command.Meta.key == "questionnaire":
384
+ # questionnaire's fields vary per questionnaire, so just check the first two fields which never vary
385
+ command_fields = command_fields[:2]
386
+ expected_fields = Command.command_schema()
387
+ assert len(command_fields) == len(expected_fields)
388
+
389
+ for actual_field in command_fields:
390
+ name = actual_field["name"]
391
+ assert name in expected_fields
392
+ expected_field = expected_fields[name]
393
+
394
+ assert expected_field["required"] == actual_field["required"]
395
+
396
+ expected_type = expected_field["type"]
397
+ if expected_type is Coding:
398
+ expected_type = expected_type.__annotations__["code"]
399
+ assert expected_type == command_type_map.get(actual_field["type"])
400
+
401
+ if (choices := actual_field["choices"]) is None:
402
+ assert expected_field["choices"] is None
403
+ continue
404
+
405
+ assert len(expected_field["choices"]) == len(choices)
406
+ for choice in choices:
407
+ assert choice["value"] in expected_field["choices"]
File without changes
@@ -0,0 +1 @@
1
+ from generated.messages.effects_pb2 import Effect, EffectType
@@ -0,0 +1,37 @@
1
+ from typing import Any
2
+
3
+ from pydantic import Field
4
+
5
+ from canvas_sdk.effects.banner_alert.constants import (
6
+ BannerAlertIntent,
7
+ BannerAlertPlacement,
8
+ )
9
+ from canvas_sdk.effects.base import _BaseEffect
10
+
11
+
12
+ class BannerAlert(_BaseEffect):
13
+ """
14
+ An Effect that will result in a banner alert in Canvas.
15
+ """
16
+
17
+ class Meta:
18
+ effect_type = "SHOW_BANNER_ALERT"
19
+
20
+ patient_key: str
21
+ narrative: str = Field(max_length=90)
22
+ placements: list[BannerAlertPlacement] = Field(min_length=1)
23
+ intents: list[BannerAlertIntent] = Field(min_length=1)
24
+
25
+ @property
26
+ def values(self) -> dict[str, Any]:
27
+ """The BannerAlert's values."""
28
+ return {
29
+ "narrative": self.narrative,
30
+ "placement": [p.value for p in self.placements],
31
+ "intent": [i.value for i in self.intents],
32
+ }
33
+
34
+ @property
35
+ def effect_payload(self) -> dict[str, Any]:
36
+ """The payload of the effect."""
37
+ return {"patient": self.patient_key, "data": self.values}
@@ -0,0 +1,19 @@
1
+ from enum import Enum
2
+
3
+
4
+ class BannerAlertPlacement(Enum):
5
+ """Where the BannerAlert should appear in the Canvas UI."""
6
+
7
+ CHART = "chart"
8
+ TIMELINE = "timeline"
9
+ APPOINTMENT_CARD = "appointment_card"
10
+ SCHEDULING_CARD = "scheduling_card"
11
+ PROFILE = "profile"
12
+
13
+
14
+ class BannerAlertIntent(Enum):
15
+ """The intent that should be conveyed in the BannerAlert."""
16
+
17
+ INFO = "info"
18
+ WARNING = "warning"
19
+ ALERT = "alert"
@@ -0,0 +1,30 @@
1
+ from typing import Any
2
+
3
+ from pydantic import BaseModel, ConfigDict
4
+
5
+ from canvas_sdk.effects import Effect
6
+
7
+
8
+ class _BaseEffect(BaseModel):
9
+ """
10
+ A Canvas Effect that changes user behavior or autonomously performs activities on behalf of users.
11
+ """
12
+
13
+ class Meta:
14
+ effect_type = ""
15
+
16
+ model_config = ConfigDict(strict=True, validate_assignment=True)
17
+
18
+ @property
19
+ def values(self) -> dict[str, Any]:
20
+ return {}
21
+
22
+ @property
23
+ def effect_payload(self) -> dict[str, Any]:
24
+ return {"data": self.values}
25
+
26
+ def apply(self) -> Effect:
27
+ return {
28
+ "type": self.Meta.effect_type,
29
+ "payload": self.effect_payload,
30
+ }
@@ -0,0 +1 @@
1
+ from generated.messages.events_pb2 import Event, EventResponse, EventType