canvas 0.7.1__py3-none-any.whl → 0.8.1__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.7.1.dist-info → canvas-0.8.1.dist-info}/METADATA +2 -2
- {canvas-0.7.1.dist-info → canvas-0.8.1.dist-info}/RECORD +58 -58
- canvas_cli/apps/auth/tests.py +10 -0
- canvas_cli/apps/emit/emit.py +0 -1
- canvas_cli/apps/logs/logs.py +4 -4
- canvas_cli/apps/plugin/plugin.py +55 -43
- canvas_cli/apps/plugin/tests.py +4 -2
- canvas_cli/conftest.py +1 -0
- canvas_cli/main.py +1 -2
- canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/protocols/my_protocol.py +1 -2
- canvas_cli/tests.py +36 -26
- canvas_cli/utils/context/context.py +4 -2
- canvas_cli/utils/context/tests.py +1 -1
- canvas_cli/utils/print/tests.py +2 -2
- canvas_cli/utils/validators/tests.py +2 -1
- canvas_generated/messages/events_pb2.py +2 -2
- canvas_generated/messages/events_pb2.pyi +68 -0
- canvas_sdk/base.py +1 -1
- canvas_sdk/commands/base.py +4 -6
- canvas_sdk/commands/commands/prescribe.py +3 -3
- canvas_sdk/commands/commands/reason_for_visit.py +1 -1
- canvas_sdk/commands/commands/task.py +3 -2
- canvas_sdk/commands/commands/vitals.py +0 -1
- canvas_sdk/commands/constants.py +3 -1
- canvas_sdk/commands/tests/protocol/tests.py +6 -1
- canvas_sdk/commands/tests/schema/tests.py +5 -1
- canvas_sdk/commands/tests/test_utils.py +27 -12
- canvas_sdk/commands/tests/unit/tests.py +3 -0
- canvas_sdk/effects/__init__.py +2 -0
- canvas_sdk/effects/banner_alert/__init__.py +2 -0
- canvas_sdk/effects/banner_alert/tests.py +22 -14
- canvas_sdk/effects/protocol_card/__init__.py +2 -0
- canvas_sdk/effects/protocol_card/tests.py +10 -6
- canvas_sdk/events/__init__.py +2 -0
- canvas_sdk/handlers/__init__.py +2 -0
- canvas_sdk/protocols/__init__.py +2 -0
- canvas_sdk/protocols/base.py +0 -2
- canvas_sdk/protocols/clinical_quality_measure.py +2 -1
- canvas_sdk/utils/http.py +2 -1
- canvas_sdk/utils/tests.py +4 -0
- canvas_sdk/v1/data/__init__.py +13 -0
- canvas_sdk/v1/data/allergy_intolerance.py +0 -1
- canvas_sdk/v1/data/base.py +5 -5
- canvas_sdk/v1/data/billing.py +2 -2
- canvas_sdk/v1/data/common.py +0 -1
- canvas_sdk/v1/data/patient.py +1 -1
- canvas_sdk/v1/data/questionnaire.py +2 -1
- canvas_sdk/value_set/custom.py +0 -10
- canvas_sdk/value_set/tests/test_value_sets.py +4 -0
- canvas_sdk/value_set/v2022/individual_characteristic.py +12 -6
- canvas_sdk/value_set/v2022/procedure.py +4 -2
- canvas_sdk/value_set/value_set.py +1 -1
- plugin_runner/plugin_runner.py +4 -3
- plugin_runner/sandbox.py +6 -8
- plugin_runner/tests/test_plugin_runner.py +1 -2
- settings.py +3 -1
- {canvas-0.7.1.dist-info → canvas-0.8.1.dist-info}/WHEEL +0 -0
- {canvas-0.7.1.dist-info → canvas-0.8.1.dist-info}/entry_points.txt +0 -0
|
@@ -18,16 +18,19 @@ from canvas_sdk.commands.tests.test_utils import (
|
|
|
18
18
|
|
|
19
19
|
@pytest.fixture(scope="session")
|
|
20
20
|
def token() -> MaskedValue:
|
|
21
|
+
"""Get a valid token."""
|
|
21
22
|
return get_token()
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
@pytest.fixture(scope="session")
|
|
25
26
|
def new_note(token: MaskedValue) -> dict:
|
|
27
|
+
"""Create a new note."""
|
|
26
28
|
return create_new_note(token)
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
@pytest.fixture
|
|
30
32
|
def command_type_map() -> dict[str, type]:
|
|
33
|
+
"""Map of command field types to their corresponding Python types."""
|
|
31
34
|
return {
|
|
32
35
|
"AutocompleteField": str,
|
|
33
36
|
"MultiLineTextField": str,
|
|
@@ -51,6 +54,7 @@ def test_command_schema_matches_command_api(
|
|
|
51
54
|
new_note: dict,
|
|
52
55
|
Command: _BaseCommand,
|
|
53
56
|
) -> None:
|
|
57
|
+
"""Test that the command schema matches the command API."""
|
|
54
58
|
# first create the command in the new note
|
|
55
59
|
data = {"noteKey": new_note["externallyExposableId"], "schemaKey": Command.Meta.key}
|
|
56
60
|
headers = {"Authorization": f"Bearer {token.value}"}
|
|
@@ -90,7 +94,7 @@ def test_command_schema_matches_command_api(
|
|
|
90
94
|
# this condition initially created for Prescribe.indications,
|
|
91
95
|
# but could apply to other AutocompleteField fields that are lists
|
|
92
96
|
# making the assumption here that if the field ends in 's' (like indications), it is a list
|
|
93
|
-
assert get_origin(expected_type)
|
|
97
|
+
assert get_origin(expected_type) is list
|
|
94
98
|
|
|
95
99
|
else:
|
|
96
100
|
assert expected_type == actual_type
|
|
@@ -35,6 +35,8 @@ runner = CliRunner()
|
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
class MaskedValue:
|
|
38
|
+
"""A class to mask sensitive values in tests."""
|
|
39
|
+
|
|
38
40
|
def __init__(self, value: str) -> None:
|
|
39
41
|
self.value = value
|
|
40
42
|
|
|
@@ -46,6 +48,7 @@ class MaskedValue:
|
|
|
46
48
|
|
|
47
49
|
|
|
48
50
|
def get_field_type_unformatted(field_props: dict[str, Any]) -> str:
|
|
51
|
+
"""Get the unformatted field type from the field properties."""
|
|
49
52
|
if t := field_props.get("type"):
|
|
50
53
|
return field_props.get("format") or t
|
|
51
54
|
|
|
@@ -56,14 +59,17 @@ def get_field_type_unformatted(field_props: dict[str, Any]) -> str:
|
|
|
56
59
|
|
|
57
60
|
|
|
58
61
|
def get_field_type(field_props: dict) -> str:
|
|
62
|
+
"""Get the field type from the field properties."""
|
|
59
63
|
return get_field_type_unformatted(field_props).replace("-", "").replace("array", "list")
|
|
60
64
|
|
|
61
65
|
|
|
62
66
|
def random_string() -> str:
|
|
67
|
+
"""Generate a random string."""
|
|
63
68
|
return "".join(random.choices(string.ascii_uppercase + string.digits, k=7))
|
|
64
69
|
|
|
65
70
|
|
|
66
71
|
def fake(field_props: dict, Command: type[_BaseCommand]) -> Any:
|
|
72
|
+
"""Generate a fake value for a field."""
|
|
67
73
|
t = get_field_type(field_props)
|
|
68
74
|
match t:
|
|
69
75
|
case "string":
|
|
@@ -89,13 +95,14 @@ def fake(field_props: dict, Command: type[_BaseCommand]) -> Any:
|
|
|
89
95
|
case "ClinicalQuantity":
|
|
90
96
|
return ClinicalQuantity(representative_ndc="ndc", ncpdp_quantity_qualifier_code="code")
|
|
91
97
|
if t[0].isupper():
|
|
92
|
-
return random.choice(
|
|
98
|
+
return random.choice(list(getattr(Command, t)))
|
|
93
99
|
|
|
94
100
|
|
|
95
101
|
def raises_wrong_type_error(
|
|
96
102
|
Command: type[_BaseCommand],
|
|
97
103
|
field: str,
|
|
98
104
|
) -> None:
|
|
105
|
+
"""Test that the correct error is raised when the wrong type is passed to a field."""
|
|
99
106
|
field_props = Command.model_json_schema()["properties"][field]
|
|
100
107
|
field_type = get_field_type(field_props)
|
|
101
108
|
wrong_field_type = "integer" if field_type == "string" else "string"
|
|
@@ -119,8 +126,8 @@ def raises_wrong_type_error(
|
|
|
119
126
|
"dictionary" if field_type == "Coding" or field_type == "ClinicalQuantity" else field_type
|
|
120
127
|
)
|
|
121
128
|
if field_type == "number":
|
|
122
|
-
assert
|
|
123
|
-
assert
|
|
129
|
+
assert "Input should be an instance of Decimal" in err_msg1
|
|
130
|
+
assert "Input should be an instance of Decimal" in err_msg2
|
|
124
131
|
elif field_type[0].isupper():
|
|
125
132
|
assert f"Input should be an instance of {Command.__name__}.{field_type}" in err_msg1
|
|
126
133
|
assert f"Input should be an instance of {Command.__name__}.{field_type}" in err_msg2
|
|
@@ -133,6 +140,7 @@ def raises_none_error_for_effect_method(
|
|
|
133
140
|
Command: type[_BaseCommand],
|
|
134
141
|
method: str,
|
|
135
142
|
) -> None:
|
|
143
|
+
"""Test that the correct error is raised when a required field is None for an effect method."""
|
|
136
144
|
cmd_name = Command.__name__
|
|
137
145
|
cmd_name_article = "an" if cmd_name.startswith(("A", "E", "I", "O", "U")) else "a"
|
|
138
146
|
|
|
@@ -155,6 +163,7 @@ def raises_none_error_for_effect_method(
|
|
|
155
163
|
def write_protocol_code(
|
|
156
164
|
note_uuid: str, plugin_name: str, commands: list[type[_BaseCommand]]
|
|
157
165
|
) -> None:
|
|
166
|
+
"""Test that the protocol code is written correctly."""
|
|
158
167
|
imports = ", ".join([c.__name__ for c in commands])
|
|
159
168
|
effects = ", ".join([f"{c.__name__}(note_uuid='{note_uuid}').originate()" for c in commands])
|
|
160
169
|
|
|
@@ -171,21 +180,23 @@ class Protocol(BaseProtocol):
|
|
|
171
180
|
with chdir(Path("./custom-plugins")):
|
|
172
181
|
runner.invoke(app, "init", input=plugin_name)
|
|
173
182
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
protocol.close()
|
|
183
|
+
with open(f"./custom-plugins/{plugin_name}/protocols/my_protocol.py", "w") as protocol:
|
|
184
|
+
protocol.write(protocol_code)
|
|
177
185
|
|
|
178
186
|
|
|
179
187
|
def install_plugin(plugin_name: str, token: MaskedValue) -> None:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
188
|
+
"""Install a plugin."""
|
|
189
|
+
with open(_build_package(Path(f"./custom-plugins/{plugin_name}")), "rb") as package:
|
|
190
|
+
requests.post(
|
|
191
|
+
plugin_url(cast(str, settings.INTEGRATION_TEST_URL)),
|
|
192
|
+
data={"is_enabled": True},
|
|
193
|
+
files={"package": package},
|
|
194
|
+
headers={"Authorization": f"Bearer {token.value}"},
|
|
195
|
+
)
|
|
186
196
|
|
|
187
197
|
|
|
188
198
|
def trigger_plugin_event(token: MaskedValue) -> None:
|
|
199
|
+
"""Trigger a plugin event."""
|
|
189
200
|
requests.post(
|
|
190
201
|
f"{settings.INTEGRATION_TEST_URL}/api/Note/",
|
|
191
202
|
headers={
|
|
@@ -204,6 +215,7 @@ def trigger_plugin_event(token: MaskedValue) -> None:
|
|
|
204
215
|
|
|
205
216
|
|
|
206
217
|
def get_original_note_body_commands(new_note_id: int, token: MaskedValue) -> list[str]:
|
|
218
|
+
"""Get the commands from the original note body."""
|
|
207
219
|
original_note = requests.get(
|
|
208
220
|
f"{settings.INTEGRATION_TEST_URL}/api/Note/{new_note_id}",
|
|
209
221
|
headers={
|
|
@@ -225,6 +237,7 @@ def get_original_note_body_commands(new_note_id: int, token: MaskedValue) -> lis
|
|
|
225
237
|
|
|
226
238
|
|
|
227
239
|
def clean_up_files_and_plugins(plugin_name: str, token: MaskedValue) -> None:
|
|
240
|
+
"""Clean up the files and plugins."""
|
|
228
241
|
# clean up
|
|
229
242
|
if Path(f"./custom-plugins/{plugin_name}").exists():
|
|
230
243
|
shutil.rmtree(Path(f"./custom-plugins/{plugin_name}"))
|
|
@@ -261,6 +274,7 @@ COMMANDS: list[type[_BaseCommand]] = [
|
|
|
261
274
|
|
|
262
275
|
|
|
263
276
|
def create_new_note(token: MaskedValue) -> dict:
|
|
277
|
+
"""Create a new note."""
|
|
264
278
|
headers = {
|
|
265
279
|
"Authorization": f"Bearer {token.value}",
|
|
266
280
|
"Content-Type": "application/json",
|
|
@@ -279,6 +293,7 @@ def create_new_note(token: MaskedValue) -> dict:
|
|
|
279
293
|
|
|
280
294
|
|
|
281
295
|
def get_token() -> MaskedValue:
|
|
296
|
+
"""Get a valid token."""
|
|
282
297
|
return MaskedValue(
|
|
283
298
|
requests.post(
|
|
284
299
|
f"{settings.INTEGRATION_TEST_URL}/auth/token/",
|
|
@@ -82,6 +82,7 @@ def test_command_raises_generic_error_when_kwarg_given_incorrect_type(
|
|
|
82
82
|
Command: type[_BaseCommand],
|
|
83
83
|
fields_to_test: tuple[str],
|
|
84
84
|
) -> None:
|
|
85
|
+
"""Test that Command raises a generic error when a kwarg is given an incorrect type."""
|
|
85
86
|
for field in fields_to_test:
|
|
86
87
|
raises_wrong_type_error(Command, field)
|
|
87
88
|
|
|
@@ -184,6 +185,7 @@ def test_command_raises_specific_error_when_kwarg_given_incorrect_type(
|
|
|
184
185
|
err_msg: str,
|
|
185
186
|
valid_kwargs: dict,
|
|
186
187
|
) -> None:
|
|
188
|
+
"""Test that Command raises a specific error when a kwarg is given an incorrect type."""
|
|
187
189
|
with pytest.raises(ValidationError) as e1:
|
|
188
190
|
cmd = Command(**err_kwargs)
|
|
189
191
|
cmd.originate()
|
|
@@ -258,6 +260,7 @@ def test_command_allows_kwarg_with_correct_type(
|
|
|
258
260
|
Command: type[_BaseCommand],
|
|
259
261
|
fields_to_test: tuple[str],
|
|
260
262
|
) -> None:
|
|
263
|
+
"""Test that Command allows a kwarg with the correct type."""
|
|
261
264
|
schema = Command.model_json_schema()
|
|
262
265
|
|
|
263
266
|
for field in fields_to_test:
|
canvas_sdk/effects/__init__.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import shutil
|
|
2
|
+
from collections.abc import Generator
|
|
2
3
|
from contextlib import chdir
|
|
3
4
|
from datetime import datetime
|
|
4
5
|
from pathlib import Path
|
|
5
|
-
from typing import Any
|
|
6
|
+
from typing import Any
|
|
6
7
|
|
|
7
8
|
import pytest
|
|
8
9
|
import requests
|
|
@@ -21,6 +22,7 @@ runner = CliRunner()
|
|
|
21
22
|
|
|
22
23
|
@pytest.fixture(scope="session")
|
|
23
24
|
def token() -> MaskedValue:
|
|
25
|
+
"""Get a valid token."""
|
|
24
26
|
return MaskedValue(
|
|
25
27
|
requests.post(
|
|
26
28
|
f"{settings.INTEGRATION_TEST_URL}/auth/token/",
|
|
@@ -36,6 +38,7 @@ def token() -> MaskedValue:
|
|
|
36
38
|
|
|
37
39
|
@pytest.fixture(scope="session")
|
|
38
40
|
def first_patient_id(token: MaskedValue) -> dict:
|
|
41
|
+
"""Get the first patient id."""
|
|
39
42
|
headers = {
|
|
40
43
|
"Authorization": f"Bearer {token.value}",
|
|
41
44
|
"Content-Type": "application/json",
|
|
@@ -47,6 +50,7 @@ def first_patient_id(token: MaskedValue) -> dict:
|
|
|
47
50
|
|
|
48
51
|
@pytest.fixture(scope="session")
|
|
49
52
|
def plugin_name() -> str:
|
|
53
|
+
"""Get the plugin name to be used."""
|
|
50
54
|
return f"addbanneralert{datetime.now().timestamp()}".replace(".", "")
|
|
51
55
|
|
|
52
56
|
|
|
@@ -54,7 +58,7 @@ def plugin_name() -> str:
|
|
|
54
58
|
def write_and_install_protocol_and_clean_up(
|
|
55
59
|
first_patient_id: str, plugin_name: str, token: MaskedValue
|
|
56
60
|
) -> Generator[Any, Any, Any]:
|
|
57
|
-
|
|
61
|
+
"""Write the protocol code, install the plugin, and clean up after the test."""
|
|
58
62
|
if not settings.INTEGRATION_TEST_URL:
|
|
59
63
|
raise ImproperlyConfigured("INTEGRATION_TEST_URL is not set")
|
|
60
64
|
|
|
@@ -62,9 +66,8 @@ def write_and_install_protocol_and_clean_up(
|
|
|
62
66
|
with chdir(Path("./custom-plugins")):
|
|
63
67
|
runner.invoke(app, "init", input=plugin_name)
|
|
64
68
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
f"""from canvas_sdk.effects.banner_alert import AddBannerAlert
|
|
69
|
+
protocol_code = f"""
|
|
70
|
+
from canvas_sdk.effects.banner_alert import AddBannerAlert
|
|
68
71
|
from canvas_sdk.events import EventType
|
|
69
72
|
from canvas_sdk.protocols import BaseProtocol
|
|
70
73
|
|
|
@@ -81,16 +84,18 @@ class Protocol(BaseProtocol):
|
|
|
81
84
|
).apply()
|
|
82
85
|
]
|
|
83
86
|
"""
|
|
84
|
-
)
|
|
85
|
-
protocol.close()
|
|
86
87
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
88
|
+
with open(f"./custom-plugins/{plugin_name}/protocols/my_protocol.py", "w") as protocol:
|
|
89
|
+
protocol.write(protocol_code)
|
|
90
|
+
|
|
91
|
+
with open(_build_package(Path(f"./custom-plugins/{plugin_name}")), "rb") as package:
|
|
92
|
+
# install the plugin
|
|
93
|
+
requests.post(
|
|
94
|
+
plugin_url(settings.INTEGRATION_TEST_URL),
|
|
95
|
+
data={"is_enabled": True},
|
|
96
|
+
files={"package": package},
|
|
97
|
+
headers={"Authorization": f"Bearer {token.value}"},
|
|
98
|
+
)
|
|
94
99
|
|
|
95
100
|
yield
|
|
96
101
|
|
|
@@ -132,6 +137,7 @@ def test_protocol_that_adds_banner_alert(
|
|
|
132
137
|
plugin_name: str,
|
|
133
138
|
first_patient_id: str,
|
|
134
139
|
) -> None:
|
|
140
|
+
"""Test that the protocol adds a banner alert."""
|
|
135
141
|
# trigger the event
|
|
136
142
|
requests.post(
|
|
137
143
|
f"{settings.INTEGRATION_TEST_URL}/api/Note/",
|
|
@@ -206,6 +212,7 @@ def test_protocol_that_adds_banner_alert(
|
|
|
206
212
|
def test_banner_alert_apply_method_succeeds_with_all_required_fields(
|
|
207
213
|
Effect: type[AddBannerAlert] | type[RemoveBannerAlert], params: dict, expected_payload: str
|
|
208
214
|
) -> None:
|
|
215
|
+
"""Test that the apply method succeeds with all required fields."""
|
|
209
216
|
b = Effect()
|
|
210
217
|
for k, v in params.items():
|
|
211
218
|
setattr(b, k, v)
|
|
@@ -240,6 +247,7 @@ def test_banner_alert_apply_method_succeeds_with_all_required_fields(
|
|
|
240
247
|
def test_banner_alert_apply_method_raises_error_without_required_fields(
|
|
241
248
|
Effect: type[AddBannerAlert] | type[RemoveBannerAlert], expected_err_msgs: str
|
|
242
249
|
) -> None:
|
|
250
|
+
"""Test that the apply method raises an error when missing required fields."""
|
|
243
251
|
b = Effect()
|
|
244
252
|
with pytest.raises(ValidationError) as e:
|
|
245
253
|
b.apply()
|
|
@@ -21,6 +21,7 @@ from canvas_sdk.effects.protocol_card import ProtocolCard, Recommendation
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def test_apply_method_succeeds_with_patient_id_and_key() -> None:
|
|
24
|
+
"""Test that the apply method succeeds with all required fields."""
|
|
24
25
|
p = ProtocolCard(patient_id="uuid", key="something-unique")
|
|
25
26
|
applied = p.apply()
|
|
26
27
|
assert (
|
|
@@ -30,6 +31,7 @@ def test_apply_method_succeeds_with_patient_id_and_key() -> None:
|
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
def test_apply_method_raises_error_without_patient_id_and_key() -> None:
|
|
34
|
+
"""Test that the apply method raises an error when missing required fields."""
|
|
33
35
|
p = ProtocolCard()
|
|
34
36
|
|
|
35
37
|
with pytest.raises(ValidationError) as e:
|
|
@@ -109,6 +111,7 @@ def test_apply_method_raises_error_without_patient_id_and_key() -> None:
|
|
|
109
111
|
def test_add_recommendations(
|
|
110
112
|
init_params: dict[Any, Any], rec1_params: dict[Any, Any], rec2_params: dict[Any, Any]
|
|
111
113
|
) -> None:
|
|
114
|
+
"""Test that the add_recommendation method adds recommendations to the ProtocolCard."""
|
|
112
115
|
p = ProtocolCard(**init_params)
|
|
113
116
|
p.add_recommendation(**rec1_params)
|
|
114
117
|
p.recommendations.append(Recommendation(**rec2_params))
|
|
@@ -117,17 +120,17 @@ def test_add_recommendations(
|
|
|
117
120
|
"narrative": init_params["narrative"],
|
|
118
121
|
"recommendations": [
|
|
119
122
|
{
|
|
120
|
-
"title": rec1_params.get("title"
|
|
121
|
-
"button": rec1_params.get("button"
|
|
122
|
-
"href": rec1_params.get("href"
|
|
123
|
+
"title": rec1_params.get("title"),
|
|
124
|
+
"button": rec1_params.get("button"),
|
|
125
|
+
"href": rec1_params.get("href"),
|
|
123
126
|
"command": {"type": rec1_params["command"]} if "command" in rec1_params else {},
|
|
124
127
|
"context": rec1_params.get("context", {}),
|
|
125
128
|
"key": 0,
|
|
126
129
|
},
|
|
127
130
|
{
|
|
128
|
-
"title": rec2_params.get("title"
|
|
129
|
-
"button": rec2_params.get("button"
|
|
130
|
-
"href": rec2_params.get("href"
|
|
131
|
+
"title": rec2_params.get("title"),
|
|
132
|
+
"button": rec2_params.get("button"),
|
|
133
|
+
"href": rec2_params.get("href"),
|
|
131
134
|
"command": {"type": rec2_params["command"]} if "command" in rec2_params else {},
|
|
132
135
|
"context": rec2_params.get("context", {}),
|
|
133
136
|
"key": 1,
|
|
@@ -158,6 +161,7 @@ def test_add_recommendations(
|
|
|
158
161
|
def test_add_recommendations_from_commands(
|
|
159
162
|
Command: type[_BaseCommand], init_params: dict[str, str]
|
|
160
163
|
) -> None:
|
|
164
|
+
"""Test that the add_recommendation method adds recommendations from commands to the ProtocolCard."""
|
|
161
165
|
cmd = Command(**init_params)
|
|
162
166
|
p = ProtocolCard(patient_id="uuid", key="commands")
|
|
163
167
|
p.recommendations.append(cmd.recommend())
|
canvas_sdk/events/__init__.py
CHANGED
canvas_sdk/handlers/__init__.py
CHANGED
canvas_sdk/protocols/__init__.py
CHANGED
canvas_sdk/protocols/base.py
CHANGED
|
@@ -88,7 +88,8 @@ class ClinicalQualityMeasure(BaseProtocol):
|
|
|
88
88
|
case EventType.CONDITION_CREATED | EventType.CONDITION_UPDATED:
|
|
89
89
|
self._patient_id = patient_id(Condition)
|
|
90
90
|
case (
|
|
91
|
-
EventType.MEDICATION_LIST_ITEM_CREATED
|
|
91
|
+
EventType.MEDICATION_LIST_ITEM_CREATED
|
|
92
|
+
| EventType.MEDICATION_LIST_ITEM_UPDATED
|
|
92
93
|
):
|
|
93
94
|
self._patient_id = patient_id(Medication)
|
|
94
95
|
case _:
|
canvas_sdk/utils/http.py
CHANGED
canvas_sdk/utils/tests.py
CHANGED
|
@@ -5,6 +5,7 @@ from canvas_sdk.utils import Http
|
|
|
5
5
|
|
|
6
6
|
@patch("requests.Session.get")
|
|
7
7
|
def test_http_get(mock_get: MagicMock) -> None:
|
|
8
|
+
"""Test that the Http.get method calls requests.get with the correct arguments."""
|
|
8
9
|
http = Http()
|
|
9
10
|
http.get("https://www.canvasmedical.com/", headers={"Authorization": "Bearer as;ldkfjdkj"})
|
|
10
11
|
mock_get.assert_called_once_with(
|
|
@@ -14,6 +15,7 @@ def test_http_get(mock_get: MagicMock) -> None:
|
|
|
14
15
|
|
|
15
16
|
@patch("requests.Session.post")
|
|
16
17
|
def test_http_post(mock_post: MagicMock) -> None:
|
|
18
|
+
"""Test that the Http.post method calls requests.post with the correct arguments."""
|
|
17
19
|
http = Http()
|
|
18
20
|
http.post(
|
|
19
21
|
"https://www.canvasmedical.com/",
|
|
@@ -31,6 +33,7 @@ def test_http_post(mock_post: MagicMock) -> None:
|
|
|
31
33
|
|
|
32
34
|
@patch("requests.Session.put")
|
|
33
35
|
def test_http_put(mock_put: MagicMock) -> None:
|
|
36
|
+
"""Test that the Http.put method calls requests.put with the correct arguments."""
|
|
34
37
|
http = Http()
|
|
35
38
|
http.put(
|
|
36
39
|
"https://www.canvasmedical.com/",
|
|
@@ -48,6 +51,7 @@ def test_http_put(mock_put: MagicMock) -> None:
|
|
|
48
51
|
|
|
49
52
|
@patch("requests.Session.patch")
|
|
50
53
|
def test_http_patch(mock_patch: MagicMock) -> None:
|
|
54
|
+
"""Test that the Http.patch method calls requests.patch with the correct arguments."""
|
|
51
55
|
http = Http()
|
|
52
56
|
http.patch(
|
|
53
57
|
"https://www.canvasmedical.com/",
|
canvas_sdk/v1/data/__init__.py
CHANGED
|
@@ -4,3 +4,16 @@ from .medication import Medication, MedicationCoding
|
|
|
4
4
|
from .patient import Patient
|
|
5
5
|
from .staff import Staff
|
|
6
6
|
from .task import Task, TaskComment, TaskLabel
|
|
7
|
+
|
|
8
|
+
__all__ = (
|
|
9
|
+
"BillingLineItem",
|
|
10
|
+
"Condition",
|
|
11
|
+
"ConditionCoding",
|
|
12
|
+
"Medication",
|
|
13
|
+
"MedicationCoding",
|
|
14
|
+
"Patient",
|
|
15
|
+
"Staff",
|
|
16
|
+
"Task",
|
|
17
|
+
"TaskComment",
|
|
18
|
+
"TaskLabel",
|
|
19
|
+
)
|
canvas_sdk/v1/data/base.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from abc import abstractmethod
|
|
2
2
|
from collections.abc import Container
|
|
3
|
-
from typing import TYPE_CHECKING, Any, Protocol, Self,
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Protocol, Self, cast
|
|
4
4
|
|
|
5
5
|
from django.db import models
|
|
6
6
|
from django.db.models import Q
|
|
@@ -50,7 +50,7 @@ class ValueSetLookupQuerySetProtocol(QuerySetProtocol):
|
|
|
50
50
|
|
|
51
51
|
@staticmethod
|
|
52
52
|
@abstractmethod
|
|
53
|
-
def codings(value_set:
|
|
53
|
+
def codings(value_set: type["ValueSet"]) -> tuple[tuple[str, set[str]]]:
|
|
54
54
|
"""A protocol method for defining codings."""
|
|
55
55
|
raise NotImplementedError
|
|
56
56
|
|
|
@@ -64,7 +64,7 @@ class ValueSetLookupQuerySetProtocol(QuerySetProtocol):
|
|
|
64
64
|
class ValueSetLookupQuerySetMixin(ValueSetLookupQuerySetProtocol):
|
|
65
65
|
"""A QuerySet mixin that can filter objects based on a ValueSet."""
|
|
66
66
|
|
|
67
|
-
def find(self, value_set:
|
|
67
|
+
def find(self, value_set: type["ValueSet"]) -> models.QuerySet[Any]:
|
|
68
68
|
"""
|
|
69
69
|
Filters conditions, medications, etc. to those found in the inherited ValueSet class that is passed.
|
|
70
70
|
|
|
@@ -84,7 +84,7 @@ class ValueSetLookupQuerySetMixin(ValueSetLookupQuerySetProtocol):
|
|
|
84
84
|
return self.filter(q_filter).distinct()
|
|
85
85
|
|
|
86
86
|
@staticmethod
|
|
87
|
-
def codings(value_set:
|
|
87
|
+
def codings(value_set: type["ValueSet"]) -> tuple[tuple[str, set[str]]]:
|
|
88
88
|
"""Provide a sequence of tuples where each tuple is a code system URL and a set of codes."""
|
|
89
89
|
values_dict = cast(dict, value_set.values)
|
|
90
90
|
return cast(
|
|
@@ -113,7 +113,7 @@ class ValueSetLookupByNameQuerySetMixin(ValueSetLookupQuerySetMixin):
|
|
|
113
113
|
"""
|
|
114
114
|
|
|
115
115
|
@staticmethod
|
|
116
|
-
def codings(value_set:
|
|
116
|
+
def codings(value_set: type["ValueSet"]) -> tuple[tuple[str, set[str]]]:
|
|
117
117
|
"""
|
|
118
118
|
Provide a sequence of tuples where each tuple is a code system name and a set of codes.
|
|
119
119
|
"""
|
canvas_sdk/v1/data/billing.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import TYPE_CHECKING
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
2
|
|
|
3
3
|
from django.db import models
|
|
4
4
|
|
|
@@ -14,7 +14,7 @@ if TYPE_CHECKING:
|
|
|
14
14
|
class BillingLineItemQuerySet(ValueSetTimeframeLookupQuerySet):
|
|
15
15
|
"""A class that adds functionality to filter BillingLineItem objects."""
|
|
16
16
|
|
|
17
|
-
def find(self, value_set:
|
|
17
|
+
def find(self, value_set: type["ValueSet"]) -> models.QuerySet:
|
|
18
18
|
"""
|
|
19
19
|
This method is overridden to use for BillingLineItem CPT codes.
|
|
20
20
|
The codes are saved as string values in the BillingLineItem.cpt field,
|
canvas_sdk/v1/data/common.py
CHANGED
canvas_sdk/v1/data/patient.py
CHANGED
|
@@ -153,7 +153,8 @@ class Interview(models.Model):
|
|
|
153
153
|
note_id = models.BigIntegerField()
|
|
154
154
|
appointment_id = models.BigIntegerField()
|
|
155
155
|
questionnaires = models.ManyToManyField( # type: ignore[var-annotated]
|
|
156
|
-
Questionnaire,
|
|
156
|
+
Questionnaire,
|
|
157
|
+
through="canvas_sdk.InterviewQuestionnaireMap", # type: ignore[misc]
|
|
157
158
|
)
|
|
158
159
|
progress_status = models.CharField()
|
|
159
160
|
created = models.DateTimeField()
|
canvas_sdk/value_set/custom.py
CHANGED
|
@@ -115,7 +115,6 @@ class Antiarrhythmics(ValueSet):
|
|
|
115
115
|
"230155",
|
|
116
116
|
"237183",
|
|
117
117
|
"243776",
|
|
118
|
-
"243776",
|
|
119
118
|
"248491",
|
|
120
119
|
"248829",
|
|
121
120
|
"250272",
|
|
@@ -129,9 +128,6 @@ class Antiarrhythmics(ValueSet):
|
|
|
129
128
|
"265785",
|
|
130
129
|
"274471",
|
|
131
130
|
"278255",
|
|
132
|
-
"278255",
|
|
133
|
-
"278255",
|
|
134
|
-
"278255",
|
|
135
131
|
"280333",
|
|
136
132
|
"281153",
|
|
137
133
|
"283306",
|
|
@@ -139,8 +135,6 @@ class Antiarrhythmics(ValueSet):
|
|
|
139
135
|
"291187",
|
|
140
136
|
"296991",
|
|
141
137
|
"444249",
|
|
142
|
-
"444249",
|
|
143
|
-
"444944",
|
|
144
138
|
"444944",
|
|
145
139
|
"449494",
|
|
146
140
|
"449496",
|
|
@@ -157,12 +151,10 @@ class Antiarrhythmics(ValueSet):
|
|
|
157
151
|
"454207",
|
|
158
152
|
"454371",
|
|
159
153
|
"545231",
|
|
160
|
-
"545231",
|
|
161
154
|
"545232",
|
|
162
155
|
"545233",
|
|
163
156
|
"545238",
|
|
164
157
|
"545239",
|
|
165
|
-
"545239",
|
|
166
158
|
"558741",
|
|
167
159
|
"558745",
|
|
168
160
|
"559416",
|
|
@@ -177,8 +169,6 @@ class Antiarrhythmics(ValueSet):
|
|
|
177
169
|
"565069",
|
|
178
170
|
"573523",
|
|
179
171
|
"583982",
|
|
180
|
-
"583982",
|
|
181
|
-
"583985",
|
|
182
172
|
"583985",
|
|
183
173
|
"590326",
|
|
184
174
|
"590375",
|
|
@@ -8,12 +8,14 @@ from canvas_sdk.value_set.value_set import CombinedValueSet
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def test_value_set_class_values_property() -> None:
|
|
11
|
+
"""Test that the value_set returns the correct values."""
|
|
11
12
|
value_set = DisordersOfTheImmuneSystem
|
|
12
13
|
assert value_set.values["ICD10CM"] == DisordersOfTheImmuneSystem.ICD10CM
|
|
13
14
|
assert value_set.values["SNOMEDCT"] == DisordersOfTheImmuneSystem.SNOMEDCT
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
def test_value_set_class_pipe_operator_with_two_value_sets() -> None:
|
|
18
|
+
"""Test that the pipe operator returns the correct values."""
|
|
17
19
|
combined_value_set: CombinedValueSet = (
|
|
18
20
|
DisordersOfTheImmuneSystem | EncephalopathyDueToChildhoodVaccination
|
|
19
21
|
)
|
|
@@ -30,6 +32,7 @@ def test_value_set_class_pipe_operator_with_two_value_sets() -> None:
|
|
|
30
32
|
|
|
31
33
|
|
|
32
34
|
def test_value_set_class_pipe_operator_with_three_value_sets() -> None:
|
|
35
|
+
"""Test that the pipe operator returns the correct values with multiple operands."""
|
|
33
36
|
combined_value_set: CombinedValueSet = (
|
|
34
37
|
DisordersOfTheImmuneSystem | EncephalopathyDueToChildhoodVaccination | Rhabdomyolysis
|
|
35
38
|
)
|
|
@@ -46,6 +49,7 @@ def test_value_set_class_pipe_operator_with_three_value_sets() -> None:
|
|
|
46
49
|
|
|
47
50
|
|
|
48
51
|
def test_value_set_class_pipe_operator_with_two_combined_value_sets() -> None:
|
|
52
|
+
"""Test that the pipe operator returns the correct values with two combined value_sets."""
|
|
49
53
|
combined_value_set_1: CombinedValueSet = (
|
|
50
54
|
DisordersOfTheImmuneSystem | EncephalopathyDueToChildhoodVaccination
|
|
51
55
|
)
|