canvas 0.8.0__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.8.0.dist-info → canvas-0.8.1.dist-info}/METADATA +2 -2
- {canvas-0.8.0.dist-info → canvas-0.8.1.dist-info}/RECORD +56 -56
- 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_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.8.0.dist-info → canvas-0.8.1.dist-info}/WHEEL +0 -0
- {canvas-0.8.0.dist-info → canvas-0.8.1.dist-info}/entry_points.txt +0 -0
canvas_cli/tests.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import shutil
|
|
3
|
+
from collections.abc import Callable, Generator
|
|
3
4
|
from datetime import datetime
|
|
4
5
|
from pathlib import Path
|
|
5
|
-
from typing import Any,
|
|
6
|
+
from typing import Any, cast
|
|
6
7
|
from unittest.mock import MagicMock, patch
|
|
7
8
|
from urllib.parse import urlparse
|
|
8
9
|
|
|
@@ -20,14 +21,13 @@ runner = CliRunner()
|
|
|
20
21
|
|
|
21
22
|
@pytest.fixture(scope="session")
|
|
22
23
|
def plugin_name() -> str:
|
|
23
|
-
"""The plugin name to be used for the canvas cli test"""
|
|
24
|
+
"""The plugin name to be used for the canvas cli test."""
|
|
24
25
|
return f"cli-{datetime.now().timestamp()}".replace(".", "")
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
@pytest.fixture(scope="session")
|
|
28
29
|
def create_or_update_config_auth_file_for_testing(plugin_name: str) -> Generator[None, None, None]:
|
|
29
30
|
"""Creates the necessary config file for auth before performing cli tests."""
|
|
30
|
-
|
|
31
31
|
if not settings.INTEGRATION_TEST_URL:
|
|
32
32
|
raise ImproperlyConfigured("INTEGRATION_TEST_URL is not set")
|
|
33
33
|
|
|
@@ -45,19 +45,27 @@ def create_or_update_config_auth_file_for_testing(plugin_name: str) -> Generator
|
|
|
45
45
|
|
|
46
46
|
temp_path = path.parent / "temp_credentials.ini"
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
open(path
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
# Read the original content
|
|
49
|
+
with open(path) as original_file:
|
|
50
|
+
original_content = original_file.read()
|
|
51
|
+
|
|
52
|
+
# Write new content to the original file
|
|
53
|
+
with open(path, "w") as original_file:
|
|
54
|
+
original_file.writelines(
|
|
55
|
+
[
|
|
56
|
+
f"[{host}]\n",
|
|
57
|
+
f"client_id={client_id}\n",
|
|
58
|
+
f"client_secret={client_secret}\n",
|
|
59
|
+
]
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Append original content to the temp file
|
|
63
|
+
with open(temp_path, "a") as temp_file:
|
|
64
|
+
temp_file.write(original_content)
|
|
57
65
|
|
|
58
66
|
yield
|
|
59
67
|
|
|
60
|
-
with open(temp_path
|
|
68
|
+
with open(temp_path) as temp:
|
|
61
69
|
original_content = temp.read()
|
|
62
70
|
with open(path, "w") as f:
|
|
63
71
|
f.write(original_content)
|
|
@@ -66,9 +74,10 @@ def create_or_update_config_auth_file_for_testing(plugin_name: str) -> Generator
|
|
|
66
74
|
|
|
67
75
|
@pytest.fixture(autouse=True, scope="session")
|
|
68
76
|
def write_plugin(plugin_name: str) -> Generator[Any, Any, Any]:
|
|
77
|
+
"""Writes a plugin to the file system."""
|
|
69
78
|
runner.invoke(app, "init", input=plugin_name)
|
|
70
|
-
|
|
71
|
-
|
|
79
|
+
|
|
80
|
+
protocol_code = """
|
|
72
81
|
from canvas_sdk.events import EventType
|
|
73
82
|
from canvas_sdk.protocols import BaseProtocol
|
|
74
83
|
from logger import log
|
|
@@ -81,8 +90,9 @@ class Protocol(BaseProtocol):
|
|
|
81
90
|
log.info(self.NARRATIVE_STRING)
|
|
82
91
|
return []
|
|
83
92
|
"""
|
|
84
|
-
|
|
85
|
-
|
|
93
|
+
|
|
94
|
+
with open(f"./{plugin_name}/protocols/my_protocol.py", "w") as protocol:
|
|
95
|
+
protocol.write(protocol_code)
|
|
86
96
|
|
|
87
97
|
yield
|
|
88
98
|
|
|
@@ -91,12 +101,12 @@ class Protocol(BaseProtocol):
|
|
|
91
101
|
|
|
92
102
|
|
|
93
103
|
def list_empty_plugins(plugin_name: str) -> tuple[str, int, list[str], list[str]]:
|
|
94
|
-
"""Step 1 - list all plugins"""
|
|
104
|
+
"""Step 1 - list all plugins."""
|
|
95
105
|
return ("list", 0, [], [f"{plugin_name}"])
|
|
96
106
|
|
|
97
107
|
|
|
98
108
|
def install_new_plugin(plugin_name: str) -> tuple[str, int, list[str], list[str]]:
|
|
99
|
-
"""Step 2 - install a new plugin"""
|
|
109
|
+
"""Step 2 - install a new plugin."""
|
|
100
110
|
return (
|
|
101
111
|
f"install {plugin_name}",
|
|
102
112
|
0,
|
|
@@ -111,12 +121,12 @@ def install_new_plugin(plugin_name: str) -> tuple[str, int, list[str], list[str]
|
|
|
111
121
|
|
|
112
122
|
|
|
113
123
|
def list_newly_installed_plugin(plugin_name: str) -> tuple[str, int, list[str], list[str]]:
|
|
114
|
-
"""Step 3 - list all plugins, including newly installed one"""
|
|
124
|
+
"""Step 3 - list all plugins, including newly installed one."""
|
|
115
125
|
return ("list", 0, [f"{plugin_name}@0.0.1 enabled"], [])
|
|
116
126
|
|
|
117
127
|
|
|
118
128
|
def disable_plugin(plugin_name: str) -> tuple[str, int, list[str], list[str]]:
|
|
119
|
-
"""Step 4 - disable plugin"""
|
|
129
|
+
"""Step 4 - disable plugin."""
|
|
120
130
|
return (
|
|
121
131
|
f"disable {plugin_name}",
|
|
122
132
|
0,
|
|
@@ -126,12 +136,12 @@ def disable_plugin(plugin_name: str) -> tuple[str, int, list[str], list[str]]:
|
|
|
126
136
|
|
|
127
137
|
|
|
128
138
|
def list_disabled_plugin(plugin_name: str) -> tuple[str, int, list[str], list[str]]:
|
|
129
|
-
"""Step 5 - list disabled plugin"""
|
|
139
|
+
"""Step 5 - list disabled plugin."""
|
|
130
140
|
return ("list", 0, [f"{plugin_name}@0.0.1 disabled"], [])
|
|
131
141
|
|
|
132
142
|
|
|
133
143
|
def enable_plugin(plugin_name: str) -> tuple[str, int, list[str], list[str]]:
|
|
134
|
-
"""Step 6 - enable the disabled plugin"""
|
|
144
|
+
"""Step 6 - enable the disabled plugin."""
|
|
135
145
|
return (
|
|
136
146
|
f"enable {plugin_name}",
|
|
137
147
|
0,
|
|
@@ -141,7 +151,7 @@ def enable_plugin(plugin_name: str) -> tuple[str, int, list[str], list[str]]:
|
|
|
141
151
|
|
|
142
152
|
|
|
143
153
|
def uninstall_enabled_plugin(plugin_name: str) -> tuple[str, int, list[str], list[str]]:
|
|
144
|
-
"""Step 7 - try to uninstall the enabled plugin"""
|
|
154
|
+
"""Step 7 - try to uninstall the enabled plugin."""
|
|
145
155
|
return (
|
|
146
156
|
f"uninstall {plugin_name}",
|
|
147
157
|
1,
|
|
@@ -154,7 +164,7 @@ def uninstall_enabled_plugin(plugin_name: str) -> tuple[str, int, list[str], lis
|
|
|
154
164
|
|
|
155
165
|
|
|
156
166
|
def uninstall_disabled_plugin(plugin_name: str) -> tuple[str, int, list[str], list[str]]:
|
|
157
|
-
"""Step 8 - disable and then uninstall the plugin"""
|
|
167
|
+
"""Step 8 - disable and then uninstall the plugin."""
|
|
158
168
|
runner.invoke(app, f"disable {plugin_name}")
|
|
159
169
|
|
|
160
170
|
return (
|
|
@@ -192,7 +202,7 @@ def test_canvas_list_install_disable_enable_uninstall(
|
|
|
192
202
|
create_or_update_config_auth_file_for_testing: None,
|
|
193
203
|
step: Callable,
|
|
194
204
|
) -> None:
|
|
195
|
-
"""Tests that the Canvas CLI can list, install, disable, enable, and uninstall a plugin"""
|
|
205
|
+
"""Tests that the Canvas CLI can list, install, disable, enable, and uninstall a plugin."""
|
|
196
206
|
mock_get_password.return_value = None
|
|
197
207
|
mock_set_password.return_value = None
|
|
198
208
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import functools
|
|
2
2
|
import json
|
|
3
|
+
from collections.abc import Callable
|
|
3
4
|
from pathlib import Path
|
|
4
|
-
from typing import Any,
|
|
5
|
+
from typing import Any, TypeVar, cast
|
|
5
6
|
|
|
6
7
|
import typer
|
|
7
8
|
|
|
@@ -12,6 +13,7 @@ F = TypeVar("F", bound=Callable)
|
|
|
12
13
|
|
|
13
14
|
class CLIContext:
|
|
14
15
|
"""Class that handles configuration across the CLI.
|
|
16
|
+
|
|
15
17
|
Includes methods for:
|
|
16
18
|
* Loading a JSON file with configuration keys into memory.
|
|
17
19
|
* Making a property transient (default, value is not persisted) or persistent (via decorators)
|
|
@@ -118,7 +120,7 @@ class CLIContext:
|
|
|
118
120
|
success=False,
|
|
119
121
|
path=str(file),
|
|
120
122
|
)
|
|
121
|
-
raise typer.Abort()
|
|
123
|
+
raise typer.Abort() from None
|
|
122
124
|
|
|
123
125
|
self._config_file_path = file
|
|
124
126
|
|
canvas_cli/utils/print/tests.py
CHANGED
|
@@ -18,7 +18,7 @@ def test_print_json_outputs_valid_json(message: Any, capfd: pytest.CaptureFixtur
|
|
|
18
18
|
try:
|
|
19
19
|
json.loads(output)
|
|
20
20
|
except ValueError as exc:
|
|
21
|
-
assert
|
|
21
|
+
assert AssertionError(f"{output} is not valid json: {exc}")
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def test_print_json_outputs_kwargs(capfd: pytest.CaptureFixture[str]) -> None:
|
|
@@ -33,7 +33,7 @@ def test_print_json_outputs_kwargs(capfd: pytest.CaptureFixture[str]) -> None:
|
|
|
33
33
|
assert json_dict.get("a_dict") == {"one": 2}
|
|
34
34
|
|
|
35
35
|
except ValueError as exc:
|
|
36
|
-
assert
|
|
36
|
+
assert AssertionError(f"{output} is not valid json: {exc}")
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
def test_print_overrides_default(capfd: pytest.CaptureFixture[str]) -> None:
|
|
@@ -5,6 +5,7 @@ from canvas_cli.utils.validators import validate_manifest_file
|
|
|
5
5
|
|
|
6
6
|
@pytest.fixture
|
|
7
7
|
def protocol_manifest_example() -> dict:
|
|
8
|
+
"""Return a valid protocol manifest example."""
|
|
8
9
|
return {
|
|
9
10
|
"sdk_version": "0.3.1",
|
|
10
11
|
"plugin_version": "1.0.1",
|
|
@@ -32,5 +33,5 @@ def protocol_manifest_example() -> dict:
|
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
def test_manifest_file_schema(protocol_manifest_example: dict) -> None:
|
|
35
|
-
"""Test that no exception raised when a valid manifest file is validated"""
|
|
36
|
+
"""Test that no exception raised when a valid manifest file is validated."""
|
|
36
37
|
validate_manifest_file(protocol_manifest_example)
|
canvas_sdk/base.py
CHANGED
|
@@ -24,7 +24,7 @@ class Model(BaseModel):
|
|
|
24
24
|
)
|
|
25
25
|
|
|
26
26
|
def _get_effect_method_required_fields(self, method: Any) -> tuple:
|
|
27
|
-
return getattr(self.Meta, f"{method}_required_fields",
|
|
27
|
+
return getattr(self.Meta, f"{method}_required_fields", ())
|
|
28
28
|
|
|
29
29
|
def _create_error_detail(self, type: str, message: str, value: Any) -> InitErrorDetails:
|
|
30
30
|
return InitErrorDetails({"type": PydanticCustomError(type, message), "input": value})
|
canvas_sdk/commands/base.py
CHANGED
|
@@ -2,11 +2,11 @@ import json
|
|
|
2
2
|
import re
|
|
3
3
|
from enum import EnumType
|
|
4
4
|
from types import NoneType, UnionType
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import Union, get_args, get_origin
|
|
6
6
|
|
|
7
7
|
from canvas_sdk.base import Model
|
|
8
8
|
from canvas_sdk.commands.constants import Coding
|
|
9
|
-
from canvas_sdk.effects import Effect
|
|
9
|
+
from canvas_sdk.effects import Effect
|
|
10
10
|
from canvas_sdk.effects.protocol_card import Recommendation
|
|
11
11
|
|
|
12
12
|
|
|
@@ -26,9 +26,7 @@ class _BaseCommand(Model):
|
|
|
26
26
|
command_uuid: str | None = None
|
|
27
27
|
|
|
28
28
|
def _get_effect_method_required_fields(self, method: str) -> tuple:
|
|
29
|
-
base_required_fields: tuple = getattr(
|
|
30
|
-
_BaseCommand.Meta, f"{method}_required_fields", tuple()
|
|
31
|
-
)
|
|
29
|
+
base_required_fields: tuple = getattr(_BaseCommand.Meta, f"{method}_required_fields", ())
|
|
32
30
|
command_required_fields = super()._get_effect_method_required_fields(method)
|
|
33
31
|
return tuple(set(base_required_fields) | set(command_required_fields))
|
|
34
32
|
|
|
@@ -72,7 +70,7 @@ class _BaseCommand(Model):
|
|
|
72
70
|
"""The schema of the command."""
|
|
73
71
|
base_properties = {"note_uuid", "command_uuid"}
|
|
74
72
|
schema = cls.model_json_schema()
|
|
75
|
-
required_fields: tuple = getattr(cls.Meta, "commit_required_fields",
|
|
73
|
+
required_fields: tuple = getattr(cls.Meta, "commit_required_fields", ())
|
|
76
74
|
return {
|
|
77
75
|
definition.get("commands_api_name", name): {
|
|
78
76
|
"required": name in required_fields,
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
1
|
from decimal import Decimal
|
|
3
2
|
from enum import Enum
|
|
4
|
-
from typing import TypeVar
|
|
5
3
|
|
|
6
4
|
from pydantic import Field, conlist
|
|
7
5
|
|
|
@@ -29,7 +27,9 @@ class PrescribeCommand(_BaseCommand):
|
|
|
29
27
|
NOT_ALLOWED = "not_allowed"
|
|
30
28
|
|
|
31
29
|
fdb_code: str | None = Field(default=None, json_schema_extra={"commands_api_name": "prescribe"})
|
|
32
|
-
icd10_codes: conlist(str, max_length=2) = Field(
|
|
30
|
+
icd10_codes: conlist(str, max_length=2) = Field( # type: ignore[valid-type]
|
|
31
|
+
[], json_schema_extra={"commands_api_name": "indications"}
|
|
32
|
+
)
|
|
33
33
|
sig: str = ""
|
|
34
34
|
days_supply: int | None = None
|
|
35
35
|
quantity_to_dispense: Decimal | float | int | None = None
|
|
@@ -24,7 +24,7 @@ class ReasonForVisitCommand(_BaseCommand):
|
|
|
24
24
|
if self.structured and not self.coding:
|
|
25
25
|
errors.append(
|
|
26
26
|
self._create_error_detail(
|
|
27
|
-
"value",
|
|
27
|
+
"value", "Structured RFV should have a coding.", self.coding
|
|
28
28
|
)
|
|
29
29
|
)
|
|
30
30
|
return errors
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from datetime import date
|
|
2
|
-
from enum import
|
|
2
|
+
from enum import StrEnum
|
|
3
|
+
from typing import NotRequired
|
|
3
4
|
|
|
4
|
-
from typing_extensions import
|
|
5
|
+
from typing_extensions import TypedDict
|
|
5
6
|
|
|
6
7
|
from canvas_sdk.commands.base import _BaseCommand as BaseCommand
|
|
7
8
|
|
canvas_sdk/commands/constants.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
from collections.abc import Generator
|
|
1
2
|
from datetime import datetime
|
|
2
|
-
from typing import Any, Generator
|
|
3
3
|
|
|
4
4
|
import pytest
|
|
5
5
|
|
|
@@ -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(scope="session")
|
|
30
32
|
def plugin_name() -> str:
|
|
33
|
+
"""The plugin name to be used."""
|
|
31
34
|
return f"commands{datetime.now().timestamp()}".replace(".", "")
|
|
32
35
|
|
|
33
36
|
|
|
@@ -35,6 +38,7 @@ def plugin_name() -> str:
|
|
|
35
38
|
def write_and_install_protocol_and_clean_up(
|
|
36
39
|
plugin_name: str, token: MaskedValue, new_note: dict
|
|
37
40
|
) -> Generator[None, None, None]:
|
|
41
|
+
"""Write the protocol code, install the plugin, and clean up after the test."""
|
|
38
42
|
write_protocol_code(new_note["externallyExposableId"], plugin_name, COMMANDS)
|
|
39
43
|
install_plugin(plugin_name, token)
|
|
40
44
|
|
|
@@ -47,6 +51,7 @@ def write_and_install_protocol_and_clean_up(
|
|
|
47
51
|
def test_protocol_that_inserts_every_command(
|
|
48
52
|
write_and_install_protocol_and_clean_up: None, token: MaskedValue, new_note: dict
|
|
49
53
|
) -> None:
|
|
54
|
+
"""Test that the protocol inserts every command."""
|
|
50
55
|
trigger_plugin_event(token)
|
|
51
56
|
|
|
52
57
|
commands_in_body = get_original_note_body_commands(new_note["id"], token)
|
|
@@ -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