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.
- canvas-0.1.5.dist-info/METADATA +176 -0
- canvas-0.1.5.dist-info/RECORD +66 -0
- {canvas-0.1.3.dist-info → canvas-0.1.5.dist-info}/WHEEL +1 -1
- canvas-0.1.5.dist-info/entry_points.txt +3 -0
- canvas_cli/apps/__init__.py +0 -0
- canvas_cli/apps/auth/__init__.py +3 -0
- canvas_cli/apps/auth/tests.py +142 -0
- canvas_cli/apps/auth/utils.py +163 -0
- canvas_cli/apps/logs/__init__.py +3 -0
- canvas_cli/apps/logs/logs.py +59 -0
- canvas_cli/apps/plugin/__init__.py +9 -0
- canvas_cli/apps/plugin/plugin.py +286 -0
- canvas_cli/apps/plugin/tests.py +32 -0
- canvas_cli/conftest.py +28 -0
- canvas_cli/main.py +78 -0
- canvas_cli/templates/plugins/default/cookiecutter.json +4 -0
- canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/CANVAS_MANIFEST.json +29 -0
- canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/README.md +12 -0
- canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/protocols/__init__.py +0 -0
- canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/protocols/my_protocol.py +55 -0
- canvas_cli/tests.py +11 -0
- canvas_cli/utils/__init__.py +0 -0
- canvas_cli/utils/context/__init__.py +3 -0
- canvas_cli/utils/context/context.py +172 -0
- canvas_cli/utils/context/tests.py +130 -0
- canvas_cli/utils/print/__init__.py +3 -0
- canvas_cli/utils/print/print.py +60 -0
- canvas_cli/utils/print/tests.py +70 -0
- canvas_cli/utils/urls/__init__.py +3 -0
- canvas_cli/utils/urls/tests.py +12 -0
- canvas_cli/utils/urls/urls.py +27 -0
- canvas_cli/utils/validators/__init__.py +3 -0
- canvas_cli/utils/validators/manifest_schema.py +80 -0
- canvas_cli/utils/validators/tests.py +36 -0
- canvas_cli/utils/validators/validators.py +40 -0
- canvas_sdk/__init__.py +0 -0
- canvas_sdk/commands/__init__.py +27 -0
- canvas_sdk/commands/base.py +118 -0
- canvas_sdk/commands/commands/assess.py +48 -0
- canvas_sdk/commands/commands/diagnose.py +44 -0
- canvas_sdk/commands/commands/goal.py +48 -0
- canvas_sdk/commands/commands/history_present_illness.py +15 -0
- canvas_sdk/commands/commands/medication_statement.py +28 -0
- canvas_sdk/commands/commands/plan.py +15 -0
- canvas_sdk/commands/commands/prescribe.py +48 -0
- canvas_sdk/commands/commands/questionnaire.py +17 -0
- canvas_sdk/commands/commands/reason_for_visit.py +36 -0
- canvas_sdk/commands/commands/stop_medication.py +18 -0
- canvas_sdk/commands/commands/update_goal.py +48 -0
- canvas_sdk/commands/constants.py +9 -0
- canvas_sdk/commands/tests/test_utils.py +195 -0
- canvas_sdk/commands/tests/tests.py +407 -0
- canvas_sdk/data/__init__.py +0 -0
- canvas_sdk/effects/__init__.py +1 -0
- canvas_sdk/effects/banner_alert/banner_alert.py +37 -0
- canvas_sdk/effects/banner_alert/constants.py +19 -0
- canvas_sdk/effects/base.py +30 -0
- canvas_sdk/events/__init__.py +1 -0
- canvas_sdk/protocols/__init__.py +1 -0
- canvas_sdk/protocols/base.py +12 -0
- canvas_sdk/tests/__init__.py +0 -0
- canvas_sdk/utils/__init__.py +3 -0
- canvas_sdk/utils/http.py +72 -0
- canvas_sdk/utils/tests.py +63 -0
- canvas_sdk/views/__init__.py +0 -0
- canvas/main.py +0 -19
- canvas-0.1.3.dist-info/METADATA +0 -285
- canvas-0.1.3.dist-info/RECORD +0 -6
- canvas-0.1.3.dist-info/entry_points.txt +0 -3
- {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
|