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
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
from enum import Enum, StrEnum
|
|
3
|
+
|
|
4
|
+
from typing_extensions import TypedDict, NotRequired
|
|
5
|
+
|
|
6
|
+
from canvas_sdk.commands.base import _BaseCommand as BaseCommand
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AssigneeType(StrEnum):
|
|
10
|
+
"""The type of assigner for a Task command."""
|
|
11
|
+
|
|
12
|
+
ROLE = "role"
|
|
13
|
+
TEAM = "team"
|
|
14
|
+
UNASSIGNED = "unassigned"
|
|
15
|
+
STAFF = "staff"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TaskAssigner(TypedDict):
|
|
19
|
+
"""A class for managing an assign for a Task command."""
|
|
20
|
+
|
|
21
|
+
to: AssigneeType
|
|
22
|
+
id: NotRequired[int]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TaskCommand(BaseCommand):
|
|
26
|
+
"""A class for managing a Task command within a specific note."""
|
|
27
|
+
|
|
28
|
+
class Meta:
|
|
29
|
+
key = "task"
|
|
30
|
+
commit_required_fields = (
|
|
31
|
+
"title",
|
|
32
|
+
"assign_to",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
title: str = ""
|
|
36
|
+
assign_to: TaskAssigner | None = None
|
|
37
|
+
due_date: date | None = None
|
|
38
|
+
comment: str | None = None
|
|
39
|
+
labels: list[str] | None = None
|
|
40
|
+
linked_items_urns: list[str] | None = None
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def values(self) -> dict:
|
|
44
|
+
"""The Task command's field values."""
|
|
45
|
+
return {
|
|
46
|
+
"title": self.title,
|
|
47
|
+
"assign_to": self.assign_to,
|
|
48
|
+
"due_date": self.due_date.isoformat() if self.due_date else None,
|
|
49
|
+
"comment": self.comment,
|
|
50
|
+
"labels": self.labels,
|
|
51
|
+
"linked_items_urns": self.linked_items_urns,
|
|
52
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from canvas_sdk.commands.base import _BaseCommand as BaseCommand
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class UpdateDiagnosisCommand(BaseCommand):
|
|
5
|
+
"""A class for managing an Update Diagnosis command within a specific note."""
|
|
6
|
+
|
|
7
|
+
class Meta:
|
|
8
|
+
key = "updateDiagnosis"
|
|
9
|
+
commit_required_fields = (
|
|
10
|
+
"condition_code",
|
|
11
|
+
"new_condition_code",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
condition_code: str | None = None
|
|
15
|
+
new_condition_code: str | None = None
|
|
16
|
+
background: str | None = None
|
|
17
|
+
narrative: str | None = None
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def values(self) -> dict:
|
|
21
|
+
"""The Update Diagnosis command's field values."""
|
|
22
|
+
return {
|
|
23
|
+
"condition_code": self.condition_code,
|
|
24
|
+
"new_condition_code": self.new_condition_code,
|
|
25
|
+
"background": self.background,
|
|
26
|
+
"narrative": self.narrative,
|
|
27
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from pydantic import conint, constr
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
from canvas_sdk.commands.base import _BaseCommand as BaseCommand
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class VitalsCommand(BaseCommand):
|
|
9
|
+
|
|
10
|
+
class Meta:
|
|
11
|
+
key = "vitals"
|
|
12
|
+
commit_required_fields = ()
|
|
13
|
+
|
|
14
|
+
class BodyTemperatureSite(Enum):
|
|
15
|
+
AXILLARY = 0
|
|
16
|
+
ORAL = 1
|
|
17
|
+
RECTAL = 2
|
|
18
|
+
TEMPORAL = 3
|
|
19
|
+
TYMPANIC = 4
|
|
20
|
+
|
|
21
|
+
class BloodPressureSite(Enum):
|
|
22
|
+
SITTING_RIGHT_UPPER = 0
|
|
23
|
+
SITTING_LEFT_UPPER = 1
|
|
24
|
+
SITTING_RIGHT_LOWER = 2
|
|
25
|
+
SITTING_LEFT_LOWER = 3
|
|
26
|
+
STANDING_RIGHT_UPPER = 4
|
|
27
|
+
STANDING_LEFT_UPPER = 5
|
|
28
|
+
STANDING_RIGHT_LOWER = 6
|
|
29
|
+
STANDING_LEFT_LOWER = 7
|
|
30
|
+
SUPINE_RIGHT_UPPER = 8
|
|
31
|
+
SUPINE_LEFT_UPPER = 9
|
|
32
|
+
SUPINE_RIGHT_LOWER = 10
|
|
33
|
+
SUPINE_LEFT_LOWER = 11
|
|
34
|
+
|
|
35
|
+
class PulseRhythm(Enum):
|
|
36
|
+
REGULAR = 0
|
|
37
|
+
IRREGULARLY_IRREGULAR = 1
|
|
38
|
+
REGULARLY_IRREGULAR = 2
|
|
39
|
+
|
|
40
|
+
height: conint(ge=10, le=108) | None = None
|
|
41
|
+
weight_lbs: conint(ge=1, le=1500) | None = None
|
|
42
|
+
weight_oz: int | None = None
|
|
43
|
+
waist_circumference: conint(ge=20, le=200) | None = None
|
|
44
|
+
body_temperature: conint(ge=85, le=107) | None = None
|
|
45
|
+
body_temperature_site: BodyTemperatureSite | None = None
|
|
46
|
+
blood_pressure_systole: conint(ge=30, le=305) | None = None
|
|
47
|
+
blood_pressure_diastole: conint(ge=20, le=180) | None = None
|
|
48
|
+
blood_pressure_position_and_site: BloodPressureSite | None = None
|
|
49
|
+
pulse: conint(ge=30, le=250) | None = None
|
|
50
|
+
pulse_rhythm: PulseRhythm | None = None
|
|
51
|
+
respiration_rate: conint(ge=6, le=60) | None = None
|
|
52
|
+
oxygen_saturation: conint(ge=60, le=100) | None = None
|
|
53
|
+
note: constr(max_length=150) | None = None
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def values(self) -> dict:
|
|
57
|
+
return {
|
|
58
|
+
"height": self.height,
|
|
59
|
+
"weight_lbs": self.weight_lbs,
|
|
60
|
+
"weight_oz": self.weight_oz,
|
|
61
|
+
"waist_circumference": self.waist_circumference,
|
|
62
|
+
"body_temperature": self.body_temperature,
|
|
63
|
+
"body_temperature_site": (
|
|
64
|
+
self.body_temperature_site.value if self.body_temperature_site else None
|
|
65
|
+
),
|
|
66
|
+
"blood_pressure_systole": self.blood_pressure_systole,
|
|
67
|
+
"blood_pressure_diastole": self.blood_pressure_diastole,
|
|
68
|
+
"blood_pressure_position_and_site": (
|
|
69
|
+
self.blood_pressure_position_and_site.value
|
|
70
|
+
if self.blood_pressure_position_and_site
|
|
71
|
+
else None
|
|
72
|
+
),
|
|
73
|
+
"pulse": self.pulse,
|
|
74
|
+
"pulse_rhythm": self.pulse_rhythm.value if self.pulse_rhythm else None,
|
|
75
|
+
"respiration_rate": self.respiration_rate,
|
|
76
|
+
"oxygen_saturation": self.oxygen_saturation,
|
|
77
|
+
"note": self.note,
|
|
78
|
+
}
|
canvas_sdk/commands/constants.py
CHANGED
|
File without changes
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Any, Generator
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from canvas_sdk.commands.tests.test_utils import (
|
|
7
|
+
COMMANDS,
|
|
8
|
+
MaskedValue,
|
|
9
|
+
clean_up_files_and_plugins,
|
|
10
|
+
create_new_note,
|
|
11
|
+
get_original_note_body_commands,
|
|
12
|
+
get_token,
|
|
13
|
+
install_plugin,
|
|
14
|
+
trigger_plugin_event,
|
|
15
|
+
write_protocol_code,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture(scope="session")
|
|
20
|
+
def token() -> MaskedValue:
|
|
21
|
+
return get_token()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture(scope="session")
|
|
25
|
+
def new_note(token: MaskedValue) -> dict:
|
|
26
|
+
return create_new_note(token)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.fixture(scope="session")
|
|
30
|
+
def plugin_name() -> str:
|
|
31
|
+
return f"commands{datetime.now().timestamp()}".replace(".", "")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.fixture(autouse=True, scope="session")
|
|
35
|
+
def write_and_install_protocol_and_clean_up(
|
|
36
|
+
plugin_name: str, token: MaskedValue, new_note: dict
|
|
37
|
+
) -> Generator[Any, Any, Any]:
|
|
38
|
+
write_protocol_code(new_note["externallyExposableId"], plugin_name, COMMANDS)
|
|
39
|
+
install_plugin(plugin_name, token)
|
|
40
|
+
|
|
41
|
+
yield
|
|
42
|
+
|
|
43
|
+
clean_up_files_and_plugins(plugin_name, token)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.mark.integtest
|
|
47
|
+
def test_protocol_that_inserts_every_command(token: MaskedValue, new_note: dict) -> None:
|
|
48
|
+
trigger_plugin_event(token)
|
|
49
|
+
|
|
50
|
+
commands_in_body = get_original_note_body_commands(new_note["id"], token)
|
|
51
|
+
command_keys = [c.Meta.key for c in COMMANDS]
|
|
52
|
+
|
|
53
|
+
assert len(command_keys) == len(commands_in_body)
|
|
54
|
+
for i, command_key in enumerate(command_keys):
|
|
55
|
+
assert commands_in_body[i] == command_key
|
|
File without changes
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import decimal
|
|
2
|
+
from datetime import date, datetime
|
|
3
|
+
from typing import get_origin
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
import settings
|
|
9
|
+
from canvas_sdk.commands.base import _BaseCommand
|
|
10
|
+
from canvas_sdk.commands.constants import Coding, ClinicalQuantity
|
|
11
|
+
from canvas_sdk.commands.tests.test_utils import (
|
|
12
|
+
COMMANDS,
|
|
13
|
+
MaskedValue,
|
|
14
|
+
create_new_note,
|
|
15
|
+
get_token,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture(scope="session")
|
|
20
|
+
def token() -> MaskedValue:
|
|
21
|
+
return get_token()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture(scope="session")
|
|
25
|
+
def new_note(token: MaskedValue) -> dict:
|
|
26
|
+
return create_new_note(token)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def command_type_map() -> dict[str, type]:
|
|
31
|
+
return {
|
|
32
|
+
"AutocompleteField": str,
|
|
33
|
+
"MultiLineTextField": str,
|
|
34
|
+
"TextField": str,
|
|
35
|
+
"ChoiceField": str,
|
|
36
|
+
"DateField": datetime,
|
|
37
|
+
"ApproximateDateField": date,
|
|
38
|
+
"IntegerField": int,
|
|
39
|
+
"DecimalField": decimal.Decimal,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.mark.integtest
|
|
44
|
+
@pytest.mark.parametrize(
|
|
45
|
+
"Command",
|
|
46
|
+
COMMANDS,
|
|
47
|
+
)
|
|
48
|
+
def test_command_schema_matches_command_api(
|
|
49
|
+
token: MaskedValue,
|
|
50
|
+
command_type_map: dict[str, str],
|
|
51
|
+
new_note: dict,
|
|
52
|
+
Command: _BaseCommand,
|
|
53
|
+
) -> None:
|
|
54
|
+
# first create the command in the new note
|
|
55
|
+
data = {"noteKey": new_note["externallyExposableId"], "schemaKey": Command.Meta.key}
|
|
56
|
+
headers = {"Authorization": f"Bearer {token.value}"}
|
|
57
|
+
url = f"{settings.INTEGRATION_TEST_URL}/core/api/v1/commands/"
|
|
58
|
+
command_resp = requests.post(url, headers=headers, data=data).json()
|
|
59
|
+
assert "uuid" in command_resp
|
|
60
|
+
command_uuid = command_resp["uuid"]
|
|
61
|
+
|
|
62
|
+
# next, request the fields of the newly created command
|
|
63
|
+
url = f"{settings.INTEGRATION_TEST_URL}/core/api/v1/commands/{command_uuid}/fields/"
|
|
64
|
+
command_fields_resp = requests.get(url, headers=headers).json()
|
|
65
|
+
assert command_fields_resp["schema"] == Command.Meta.key
|
|
66
|
+
|
|
67
|
+
command_fields = command_fields_resp["fields"]
|
|
68
|
+
if Command.Meta.key == "questionnaire":
|
|
69
|
+
# questionnaire's fields vary per questionnaire, so just check the first two fields which never vary
|
|
70
|
+
command_fields = command_fields[:2]
|
|
71
|
+
expected_fields = Command.command_schema()
|
|
72
|
+
assert len(command_fields) == len(expected_fields)
|
|
73
|
+
|
|
74
|
+
for actual_field in command_fields:
|
|
75
|
+
name = actual_field["name"]
|
|
76
|
+
assert name in expected_fields
|
|
77
|
+
expected_field = expected_fields[name]
|
|
78
|
+
|
|
79
|
+
assert expected_field["required"] == actual_field["required"]
|
|
80
|
+
|
|
81
|
+
expected_type = expected_field["type"]
|
|
82
|
+
if expected_type is Coding:
|
|
83
|
+
expected_type = expected_type.__annotations__["code"]
|
|
84
|
+
|
|
85
|
+
if expected_type is ClinicalQuantity:
|
|
86
|
+
expected_type = expected_type.__annotations__["representative_ndc"]
|
|
87
|
+
|
|
88
|
+
actual_type = command_type_map.get(actual_field["type"])
|
|
89
|
+
if actual_field["type"] == "AutocompleteField" and name[-1] == "s":
|
|
90
|
+
# this condition initially created for Prescribe.indications,
|
|
91
|
+
# but could apply to other AutocompleteField fields that are lists
|
|
92
|
+
# making the assumption here that if the field ends in 's' (like indications), it is a list
|
|
93
|
+
assert get_origin(expected_type) == list
|
|
94
|
+
|
|
95
|
+
else:
|
|
96
|
+
assert expected_type == actual_type
|
|
97
|
+
|
|
98
|
+
if (choices := actual_field["choices"]) is None:
|
|
99
|
+
assert expected_field["choices"] is None
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
assert len(expected_field["choices"]) == len(choices)
|
|
103
|
+
for choice in choices:
|
|
104
|
+
assert choice["value"] in expected_field["choices"]
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import random
|
|
2
|
+
import shutil
|
|
2
3
|
import string
|
|
4
|
+
from contextlib import chdir
|
|
3
5
|
from datetime import datetime
|
|
4
6
|
from decimal import Decimal
|
|
7
|
+
from pathlib import Path
|
|
5
8
|
from typing import Any
|
|
6
9
|
|
|
7
10
|
import pytest
|
|
11
|
+
import requests
|
|
8
12
|
from pydantic import ValidationError
|
|
13
|
+
from typer.testing import CliRunner
|
|
9
14
|
|
|
15
|
+
import settings
|
|
16
|
+
from canvas_cli.apps.plugin.plugin import _build_package, plugin_url
|
|
17
|
+
from canvas_cli.main import app
|
|
10
18
|
from canvas_sdk.commands import (
|
|
11
19
|
AssessCommand,
|
|
12
20
|
DiagnoseCommand,
|
|
@@ -18,12 +26,16 @@ from canvas_sdk.commands import (
|
|
|
18
26
|
QuestionnaireCommand,
|
|
19
27
|
ReasonForVisitCommand,
|
|
20
28
|
StopMedicationCommand,
|
|
29
|
+
UpdateGoalCommand,
|
|
21
30
|
)
|
|
22
|
-
from canvas_sdk.commands.
|
|
31
|
+
from canvas_sdk.commands.base import _BaseCommand
|
|
32
|
+
from canvas_sdk.commands.constants import Coding, ClinicalQuantity
|
|
33
|
+
|
|
34
|
+
runner = CliRunner()
|
|
23
35
|
|
|
24
36
|
|
|
25
37
|
class MaskedValue:
|
|
26
|
-
def __init__(self, value):
|
|
38
|
+
def __init__(self, value: str) -> None:
|
|
27
39
|
self.value = value
|
|
28
40
|
|
|
29
41
|
def __repr__(self) -> str:
|
|
@@ -82,8 +94,14 @@ def fake(
|
|
|
82
94
|
num_items = random.randint(0, 5)
|
|
83
95
|
item_props = field_props["anyOf"][0]["items"]
|
|
84
96
|
return [fake(item_props, Command) for i in range(num_items)]
|
|
97
|
+
case "list":
|
|
98
|
+
num_items = random.randint(0, field_props.get("maxItems", 5))
|
|
99
|
+
item_props = field_props.get("items")
|
|
100
|
+
return [fake(item_props, Command) for i in range(num_items)] if item_props else []
|
|
85
101
|
case "Coding":
|
|
86
102
|
return Coding(system=random_string(), code=random_string(), display=random_string())
|
|
103
|
+
case "ClinicalQuantity":
|
|
104
|
+
return ClinicalQuantity(representative_ndc="ndc", ncpdp_quantity_qualifier_code="code")
|
|
87
105
|
if t[0].isupper():
|
|
88
106
|
return random.choice([e for e in getattr(Command, t)])
|
|
89
107
|
|
|
@@ -122,7 +140,9 @@ def raises_wrong_type_error(
|
|
|
122
140
|
assert f"1 validation error for {Command.__name__}\n{field}" in err_msg1
|
|
123
141
|
assert f"1 validation error for {Command.__name__}\n{field}" in err_msg2
|
|
124
142
|
|
|
125
|
-
field_type =
|
|
143
|
+
field_type = (
|
|
144
|
+
"dictionary" if field_type == "Coding" or field_type == "ClinicalQuantity" else field_type
|
|
145
|
+
)
|
|
126
146
|
if field_type == "number":
|
|
127
147
|
assert f"Input should be an instance of Decimal" in err_msg1
|
|
128
148
|
assert f"Input should be an instance of Decimal" in err_msg2
|
|
@@ -149,14 +169,158 @@ def raises_none_error_for_effect_method(
|
|
|
149
169
|
),
|
|
150
170
|
method: str,
|
|
151
171
|
) -> None:
|
|
172
|
+
cmd_name = Command.__name__
|
|
173
|
+
cmd_name_article = "an" if cmd_name.startswith(("A", "E", "I", "O", "U")) else "a"
|
|
174
|
+
|
|
152
175
|
cmd = Command()
|
|
153
176
|
method_required_fields = cmd._get_effect_method_required_fields(method)
|
|
154
177
|
with pytest.raises(ValidationError) as e:
|
|
155
178
|
getattr(cmd, method)()
|
|
156
179
|
e_msg = repr(e.value)
|
|
157
|
-
|
|
158
|
-
|
|
180
|
+
missing_fields = [field for field in method_required_fields if getattr(cmd, field) is None]
|
|
181
|
+
num_errs = len(missing_fields)
|
|
182
|
+
assert f"{num_errs} validation error{'s' if num_errs > 1 else ''} for {cmd_name}" in e_msg
|
|
183
|
+
|
|
184
|
+
for f in missing_fields:
|
|
159
185
|
assert (
|
|
160
|
-
f"Field '{f}' is required to {method.replace('_', ' ')}
|
|
186
|
+
f"Field '{f}' is required to {method.replace('_', ' ')} {cmd_name_article} {cmd_name} [type=missing, input_value=None, input_type=NoneType]"
|
|
161
187
|
in e_msg
|
|
162
188
|
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def write_protocol_code(note_uuid: str, plugin_name: str, commands: list[_BaseCommand]) -> None:
|
|
192
|
+
imports = ", ".join([c.__name__ for c in commands])
|
|
193
|
+
effects = ", ".join([f"{c.__name__}(note_uuid='{note_uuid}').originate()" for c in commands])
|
|
194
|
+
|
|
195
|
+
protocol_code = f"""from canvas_sdk.commands import {imports}
|
|
196
|
+
from canvas_sdk.events import EventType
|
|
197
|
+
from canvas_sdk.protocols import BaseProtocol
|
|
198
|
+
|
|
199
|
+
class Protocol(BaseProtocol):
|
|
200
|
+
RESPONDS_TO = EventType.Name(EventType.ENCOUNTER_CREATED)
|
|
201
|
+
def compute(self):
|
|
202
|
+
return [{effects}]
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
with chdir(Path("./custom-plugins")):
|
|
206
|
+
runner.invoke(app, "init", input=plugin_name)
|
|
207
|
+
|
|
208
|
+
protocol = open(f"./custom-plugins/{plugin_name}/protocols/my_protocol.py", "w")
|
|
209
|
+
protocol.write(protocol_code)
|
|
210
|
+
protocol.close()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def install_plugin(plugin_name: str, token: MaskedValue) -> None:
|
|
214
|
+
requests.post(
|
|
215
|
+
plugin_url(settings.INTEGRATION_TEST_URL),
|
|
216
|
+
data={"is_enabled": True},
|
|
217
|
+
files={"package": open(_build_package(Path(f"./custom-plugins/{plugin_name}")), "rb")},
|
|
218
|
+
headers={"Authorization": f"Bearer {token.value}"},
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def trigger_plugin_event(token: MaskedValue) -> None:
|
|
223
|
+
requests.post(
|
|
224
|
+
f"{settings.INTEGRATION_TEST_URL}/api/Note/",
|
|
225
|
+
headers={
|
|
226
|
+
"Authorization": f"Bearer {token.value}",
|
|
227
|
+
"Content-Type": "application/json",
|
|
228
|
+
"Accept": "application/json",
|
|
229
|
+
},
|
|
230
|
+
json={
|
|
231
|
+
"patient": 2,
|
|
232
|
+
"provider": 1,
|
|
233
|
+
"note_type": "office",
|
|
234
|
+
"note_type_version": 1,
|
|
235
|
+
"lastModifiedBySessionKey": "8fee3c03a525cebee1d8a6b8e63dd4dg",
|
|
236
|
+
},
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def get_original_note_body_commands(new_note_id: int, token: MaskedValue) -> list[str]:
|
|
241
|
+
original_note = requests.get(
|
|
242
|
+
f"{settings.INTEGRATION_TEST_URL}/api/Note/{new_note_id}",
|
|
243
|
+
headers={
|
|
244
|
+
"Authorization": f"Bearer {token.value}",
|
|
245
|
+
"Content-Type": "application/json",
|
|
246
|
+
"Accept": "application/json",
|
|
247
|
+
},
|
|
248
|
+
).json()
|
|
249
|
+
|
|
250
|
+
body = original_note["body"]
|
|
251
|
+
return [
|
|
252
|
+
line["value"]
|
|
253
|
+
for line in body
|
|
254
|
+
if "data" in line
|
|
255
|
+
and "commandUuid" in line["data"]
|
|
256
|
+
and "id" in line["data"]
|
|
257
|
+
and line["type"] == "command"
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def clean_up_files_and_plugins(plugin_name: str, token: MaskedValue) -> None:
|
|
262
|
+
# clean up
|
|
263
|
+
if Path(f"./custom-plugins/{plugin_name}").exists():
|
|
264
|
+
shutil.rmtree(Path(f"./custom-plugins/{plugin_name}"))
|
|
265
|
+
|
|
266
|
+
# disable
|
|
267
|
+
requests.patch(
|
|
268
|
+
plugin_url(settings.INTEGRATION_TEST_URL, plugin_name),
|
|
269
|
+
data={"is_enabled": False},
|
|
270
|
+
headers={
|
|
271
|
+
"Authorization": f"Bearer {token.value}",
|
|
272
|
+
},
|
|
273
|
+
)
|
|
274
|
+
# delete
|
|
275
|
+
requests.delete(
|
|
276
|
+
plugin_url(settings.INTEGRATION_TEST_URL, plugin_name),
|
|
277
|
+
headers={"Authorization": f"Bearer {token.value}"},
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# For reuse with the protocol code
|
|
282
|
+
COMMANDS = [
|
|
283
|
+
AssessCommand,
|
|
284
|
+
DiagnoseCommand,
|
|
285
|
+
GoalCommand,
|
|
286
|
+
HistoryOfPresentIllnessCommand,
|
|
287
|
+
MedicationStatementCommand,
|
|
288
|
+
PlanCommand,
|
|
289
|
+
PrescribeCommand,
|
|
290
|
+
QuestionnaireCommand,
|
|
291
|
+
ReasonForVisitCommand,
|
|
292
|
+
StopMedicationCommand,
|
|
293
|
+
UpdateGoalCommand,
|
|
294
|
+
]
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def create_new_note(token: MaskedValue) -> dict:
|
|
298
|
+
headers = {
|
|
299
|
+
"Authorization": f"Bearer {token.value}",
|
|
300
|
+
"Content-Type": "application/json",
|
|
301
|
+
"Accept": "application/json",
|
|
302
|
+
}
|
|
303
|
+
data = {
|
|
304
|
+
"patient": 1,
|
|
305
|
+
"provider": 1,
|
|
306
|
+
"note_type": "office",
|
|
307
|
+
"note_type_version": 1,
|
|
308
|
+
"lastModifiedBySessionKey": "8fee3c03a525cebee1d8a6b8e63dd4dg",
|
|
309
|
+
}
|
|
310
|
+
return requests.post(
|
|
311
|
+
f"{settings.INTEGRATION_TEST_URL}/api/Note/", headers=headers, json=data
|
|
312
|
+
).json()
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def get_token() -> MaskedValue:
|
|
316
|
+
return MaskedValue(
|
|
317
|
+
requests.post(
|
|
318
|
+
f"{settings.INTEGRATION_TEST_URL}/auth/token/",
|
|
319
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
320
|
+
data={
|
|
321
|
+
"grant_type": "client_credentials",
|
|
322
|
+
"client_id": settings.INTEGRATION_TEST_CLIENT_ID,
|
|
323
|
+
"client_secret": settings.INTEGRATION_TEST_CLIENT_SECRET,
|
|
324
|
+
},
|
|
325
|
+
).json()["access_token"]
|
|
326
|
+
)
|
|
File without changes
|