canvas 0.1.15__py3-none-any.whl → 0.2.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of canvas might be problematic. Click here for more details.
- {canvas-0.1.15.dist-info → canvas-0.2.10.dist-info}/METADATA +7 -1
- canvas-0.2.10.dist-info/RECORD +143 -0
- canvas_cli/apps/plugin/plugin.py +51 -9
- canvas_cli/apps/plugin/tests.py +51 -0
- canvas_cli/tests.py +193 -4
- canvas_cli/utils/validators/manifest_schema.py +1 -0
- canvas_generated/messages/effects_pb2.py +2 -2
- canvas_generated/messages/effects_pb2.pyi +138 -0
- canvas_generated/messages/events_pb2.py +3 -3
- canvas_generated/messages/events_pb2.pyi +616 -0
- canvas_sdk/__init__.py +7 -0
- canvas_sdk/base.py +6 -2
- canvas_sdk/commands/__init__.py +26 -0
- canvas_sdk/commands/base.py +35 -32
- canvas_sdk/commands/commands/allergy.py +49 -0
- canvas_sdk/commands/commands/assess.py +1 -1
- canvas_sdk/commands/commands/close_goal.py +22 -0
- canvas_sdk/commands/commands/diagnose.py +3 -3
- canvas_sdk/commands/commands/family_history.py +18 -0
- canvas_sdk/commands/commands/goal.py +3 -3
- canvas_sdk/commands/commands/history_present_illness.py +1 -1
- canvas_sdk/commands/commands/instruct.py +17 -0
- canvas_sdk/commands/commands/lab_order.py +33 -0
- canvas_sdk/commands/commands/medical_history.py +34 -0
- canvas_sdk/commands/commands/medication_statement.py +1 -1
- canvas_sdk/commands/commands/past_surgical_history.py +28 -0
- canvas_sdk/commands/commands/perform.py +17 -0
- canvas_sdk/commands/commands/plan.py +2 -2
- canvas_sdk/commands/commands/prescribe.py +10 -7
- canvas_sdk/commands/commands/questionnaire.py +1 -1
- canvas_sdk/commands/commands/refill.py +16 -0
- canvas_sdk/commands/commands/remove_allergy.py +26 -0
- canvas_sdk/commands/commands/stop_medication.py +1 -1
- canvas_sdk/commands/commands/task.py +52 -0
- canvas_sdk/commands/commands/update_diagnosis.py +27 -0
- canvas_sdk/commands/commands/update_goal.py +1 -1
- canvas_sdk/commands/commands/vitals.py +78 -0
- canvas_sdk/commands/constants.py +7 -0
- canvas_sdk/commands/tests/protocol/__init__.py +0 -0
- canvas_sdk/commands/tests/protocol/tests.py +55 -0
- canvas_sdk/commands/tests/schema/__init__.py +0 -0
- canvas_sdk/commands/tests/schema/tests.py +104 -0
- canvas_sdk/commands/tests/test_utils.py +170 -6
- canvas_sdk/commands/tests/unit/__init__.py +0 -0
- canvas_sdk/commands/tests/{tests.py → unit/tests.py} +20 -194
- canvas_sdk/data/client.py +82 -0
- canvas_sdk/data/patient.py +1 -21
- canvas_sdk/effects/banner_alert/add_banner_alert.py +8 -7
- canvas_sdk/effects/banner_alert/remove_banner_alert.py +3 -2
- canvas_sdk/effects/banner_alert/tests.py +224 -0
- canvas_sdk/effects/base.py +3 -5
- canvas_sdk/effects/patient_chart_summary_configuration.py +39 -0
- canvas_sdk/effects/protocol_card/__init__.py +1 -0
- canvas_sdk/effects/protocol_card/protocol_card.py +83 -0
- canvas_sdk/effects/protocol_card/tests.py +184 -0
- canvas_sdk/handlers/base.py +14 -1
- canvas_sdk/protocols/base.py +14 -0
- canvas_sdk/protocols/clinical_quality_measure.py +41 -0
- canvas_sdk/utils/db.py +17 -0
- canvas_sdk/v1/__init__.py +0 -0
- canvas_sdk/v1/data/__init__.py +3 -0
- canvas_sdk/v1/data/allergy_intolerance.py +63 -0
- canvas_sdk/v1/data/base.py +47 -0
- canvas_sdk/v1/data/condition.py +48 -0
- canvas_sdk/v1/data/lab.py +96 -0
- canvas_sdk/v1/data/medication.py +54 -0
- canvas_sdk/v1/data/patient.py +49 -0
- canvas_sdk/v1/data/user.py +10 -0
- canvas_sdk/value_set/tests/test_value_sets.py +65 -0
- canvas_sdk/value_set/v2022/adverse_event.py +33 -0
- canvas_sdk/value_set/v2022/allergy.py +232 -0
- canvas_sdk/value_set/v2022/assessment.py +215 -0
- canvas_sdk/value_set/v2022/communication.py +325 -0
- canvas_sdk/value_set/v2022/condition.py +40654 -0
- canvas_sdk/value_set/v2022/device.py +174 -0
- canvas_sdk/value_set/v2022/diagnostic_study.py +4967 -0
- canvas_sdk/value_set/v2022/encounter.py +2564 -0
- canvas_sdk/value_set/v2022/immunization.py +341 -0
- canvas_sdk/value_set/v2022/individual_characteristic.py +307 -0
- canvas_sdk/value_set/v2022/intervention.py +1356 -0
- canvas_sdk/value_set/v2022/laboratory_test.py +1250 -0
- canvas_sdk/value_set/v2022/medication.py +5130 -0
- canvas_sdk/value_set/v2022/physical_exam.py +201 -0
- canvas_sdk/value_set/v2022/procedure.py +4037 -0
- canvas_sdk/value_set/v2022/symptom.py +176 -0
- canvas_sdk/value_set/value_set.py +91 -0
- canvas-0.1.15.dist-info/RECORD +0 -95
- canvas_generated/data_access_layer/data_access_layer_pb2.py +0 -30
- canvas_generated/data_access_layer/data_access_layer_pb2.pyi +0 -23
- canvas_generated/data_access_layer/data_access_layer_pb2_grpc.py +0 -66
- canvas_sdk/data/data_access_layer_client.py +0 -95
- canvas_sdk/data/exceptions.py +0 -16
- {canvas-0.1.15.dist-info → canvas-0.2.10.dist-info}/WHEEL +0 -0
- {canvas-0.1.15.dist-info → canvas-0.2.10.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.
|
|
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", "
|
|
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", "
|
|
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"
|
|
99
|
+
{"narrative": "yo", "note_uuid": "00000000-0000-0000-0000-000000000000"},
|
|
115
100
|
),
|
|
116
101
|
(
|
|
117
102
|
PlanCommand,
|
|
118
|
-
{"narrative": "yo", "
|
|
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", "
|
|
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", "
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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", "
|
|
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"]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from typing import Any, cast
|
|
2
|
+
|
|
3
|
+
from gql import Client, gql
|
|
4
|
+
from gql.transport.aiohttp import AIOHTTPTransport
|
|
5
|
+
|
|
6
|
+
from settings import GRAPHQL_ENDPOINT
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class _CanvasGQLClient:
|
|
10
|
+
"""
|
|
11
|
+
This is a GraphQL client that can be used to query home-app in order to fetch data for use in plugins.
|
|
12
|
+
|
|
13
|
+
Usage Examples:
|
|
14
|
+
|
|
15
|
+
A query with no parameters:
|
|
16
|
+
|
|
17
|
+
TEST_QUERY_NO_PARAMS = '''
|
|
18
|
+
query PatientsAll {
|
|
19
|
+
patients {
|
|
20
|
+
edges {
|
|
21
|
+
node {
|
|
22
|
+
firstName
|
|
23
|
+
lastName
|
|
24
|
+
birthDate
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
'''
|
|
30
|
+
|
|
31
|
+
client = _CanvasGQLClient()
|
|
32
|
+
result = client.query(TEST_QUERY_NO_PARAMS)
|
|
33
|
+
print(result) # returns dictionary
|
|
34
|
+
|
|
35
|
+
A query with parameters:
|
|
36
|
+
|
|
37
|
+
TEST_QUERY_WITH_PARAMS = '''
|
|
38
|
+
query PatientGet($patientKey: String!) {
|
|
39
|
+
patient(patientKey: $patientKey) {
|
|
40
|
+
firstName
|
|
41
|
+
lastName
|
|
42
|
+
birthDate
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
'''
|
|
46
|
+
|
|
47
|
+
client = _CanvasGQLClient()
|
|
48
|
+
result = client.query(TEST_QUERY_NO_PARAMS)
|
|
49
|
+
print(result)
|
|
50
|
+
|
|
51
|
+
For use in plugins, it is included in the instantiation of Protocol class. This means
|
|
52
|
+
it can simply be referred to as self.client in plugin code.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self) -> None:
|
|
56
|
+
self.client = Client(
|
|
57
|
+
transport=AIOHTTPTransport(url=cast(str, GRAPHQL_ENDPOINT)),
|
|
58
|
+
# TODO: follow the documentation in the link below to specify a
|
|
59
|
+
# cached copy of the schema
|
|
60
|
+
# https://gql.readthedocs.io/en/stable/usage/validation.html#using-a-provided-schema
|
|
61
|
+
fetch_schema_from_transport=False,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def query(
|
|
65
|
+
self,
|
|
66
|
+
gql_query: str,
|
|
67
|
+
variables: dict[str, Any] | None = None,
|
|
68
|
+
extra_args: dict[str, Any] | None = None
|
|
69
|
+
) -> dict[str, Any]:
|
|
70
|
+
if variables is None:
|
|
71
|
+
query_variables = {}
|
|
72
|
+
else:
|
|
73
|
+
query_variables = variables
|
|
74
|
+
|
|
75
|
+
return self.client.execute(
|
|
76
|
+
gql(gql_query),
|
|
77
|
+
variable_values=query_variables,
|
|
78
|
+
extra_args=extra_args,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
GQL_CLIENT = _CanvasGQLClient()
|
canvas_sdk/data/patient.py
CHANGED
|
@@ -1,26 +1,6 @@
|
|
|
1
|
-
from datetime import date
|
|
2
|
-
from typing import Self
|
|
3
|
-
|
|
4
1
|
from canvas_sdk.data import DataModel
|
|
5
2
|
|
|
6
|
-
from .data_access_layer_client import DAL_CLIENT
|
|
7
|
-
|
|
8
3
|
|
|
9
4
|
class Patient(DataModel):
|
|
10
|
-
"""Patient model."""
|
|
11
|
-
|
|
12
5
|
id: str | None = None
|
|
13
|
-
|
|
14
|
-
last_name: str | None = None
|
|
15
|
-
birth_date: date | None = None
|
|
16
|
-
|
|
17
|
-
@classmethod
|
|
18
|
-
def get(cls, id: str) -> Self:
|
|
19
|
-
"""Given an ID, get the Patient from the Data Access Layer."""
|
|
20
|
-
patient = DAL_CLIENT.get_patient(id)
|
|
21
|
-
return cls(
|
|
22
|
-
id=patient.id,
|
|
23
|
-
first_name=patient.first_name or None,
|
|
24
|
-
last_name=patient.last_name or None,
|
|
25
|
-
birth_date=date.fromisoformat(patient.birth_date) if patient.birth_date else None,
|
|
26
|
-
)
|
|
6
|
+
# TODO - populate more attributes
|
|
@@ -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]:
|