canvas 0.1.4__py3-none-any.whl → 0.1.6__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.6.dist-info/METADATA +176 -0
- canvas-0.1.6.dist-info/RECORD +66 -0
- {canvas-0.1.4.dist-info → canvas-0.1.6.dist-info}/WHEEL +1 -1
- canvas-0.1.6.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.4.dist-info/METADATA +0 -285
- canvas-0.1.4.dist-info/RECORD +0 -6
- canvas-0.1.4.dist-info/entry_points.txt +0 -3
- {canvas → canvas_cli}/__init__.py +0 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Callable, TypeVar
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from canvas_cli.utils.print import print
|
|
9
|
+
|
|
10
|
+
F = TypeVar("F", bound=Callable)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CLIContext:
|
|
14
|
+
"""Class that handles configuration across the CLI.
|
|
15
|
+
Includes methods for:
|
|
16
|
+
* Loading a JSON file with configuration keys into memory.
|
|
17
|
+
* Making a property transient (default, value is not persisted) or persistent (via decorators)
|
|
18
|
+
|
|
19
|
+
Loading from a file is dynamic. I.e., you only need to create a new property,
|
|
20
|
+
and mark its setter as persistent if needed.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
# Path to the config file
|
|
24
|
+
_config_file_path: Path
|
|
25
|
+
|
|
26
|
+
# Dict with the config file values
|
|
27
|
+
_config_file: dict[str, Any]
|
|
28
|
+
|
|
29
|
+
# Base dir for the paths inside the project, not configurable
|
|
30
|
+
_base_dir = Path(__file__).parent / ".." / ".."
|
|
31
|
+
|
|
32
|
+
# Base dir for every template used, not configurable
|
|
33
|
+
_base_template_dir: Path = _base_dir / "templates"
|
|
34
|
+
|
|
35
|
+
# Dir where the plugin templates are located
|
|
36
|
+
_plugin_template_dir: Path = _base_template_dir / "plugins"
|
|
37
|
+
|
|
38
|
+
# Default plugin template name
|
|
39
|
+
_default_plugin_template_name: str = "default"
|
|
40
|
+
|
|
41
|
+
# The default host to use for requests
|
|
42
|
+
_default_host: str | None = None
|
|
43
|
+
|
|
44
|
+
# Print extra output
|
|
45
|
+
_verbose: bool = False
|
|
46
|
+
|
|
47
|
+
# If True no colored output is shown
|
|
48
|
+
_no_ansi: bool = False
|
|
49
|
+
|
|
50
|
+
# When the most recently requested api_token will expire
|
|
51
|
+
_token_expiration_date: str | None = None
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def persistent(fn: F | None = None, **options: Any) -> Callable[[F], F] | F:
|
|
55
|
+
"""A decorator to store a config value in the file everytime it's changed."""
|
|
56
|
+
|
|
57
|
+
def _decorator(fn: F) -> F:
|
|
58
|
+
@functools.wraps(fn)
|
|
59
|
+
def wrapper(self: "CLIContext", *args: Any, **kwargs: Any) -> Any:
|
|
60
|
+
fn(self, *args, **kwargs)
|
|
61
|
+
value = args[0]
|
|
62
|
+
|
|
63
|
+
print.verbose(f"Storing {fn.__name__}={value} in the config file")
|
|
64
|
+
|
|
65
|
+
self._config_file[fn.__name__] = value
|
|
66
|
+
with open(self._config_file_path, "w") as f:
|
|
67
|
+
json.dump(self._config_file, f)
|
|
68
|
+
|
|
69
|
+
return wrapper
|
|
70
|
+
|
|
71
|
+
return _decorator(fn) if fn else _decorator
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def plugin_template_dir(self) -> Path:
|
|
75
|
+
"""Default Path to use for Plugin templates."""
|
|
76
|
+
return self._plugin_template_dir
|
|
77
|
+
|
|
78
|
+
@plugin_template_dir.setter
|
|
79
|
+
@persistent
|
|
80
|
+
def plugin_template_dir(self, new_plugin_template_dir: Path) -> None:
|
|
81
|
+
self._plugin_template_dir = new_plugin_template_dir
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def default_plugin_template_name(self) -> str:
|
|
85
|
+
"""Default template to be used when creating a Plugin."""
|
|
86
|
+
return self._default_plugin_template_name
|
|
87
|
+
|
|
88
|
+
@default_plugin_template_name.setter
|
|
89
|
+
@persistent
|
|
90
|
+
def default_plugin_template_name(self, new_default_plugin_template_name: str) -> None:
|
|
91
|
+
self._default_plugin_template_name = new_default_plugin_template_name
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def default_host(self) -> str | None:
|
|
95
|
+
"""Default host to be used when connecting to instances."""
|
|
96
|
+
return self._default_host
|
|
97
|
+
|
|
98
|
+
@default_host.setter
|
|
99
|
+
@persistent
|
|
100
|
+
def default_host(self, new_default_host: str | None) -> None:
|
|
101
|
+
self._default_host = new_default_host
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def verbose(self) -> bool:
|
|
105
|
+
"""Enable extra output."""
|
|
106
|
+
return self._verbose
|
|
107
|
+
|
|
108
|
+
@verbose.setter
|
|
109
|
+
def verbose(self, new_verbose: bool) -> None:
|
|
110
|
+
self._verbose = new_verbose
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def no_ansi(self) -> bool:
|
|
114
|
+
"""If set removes colorized output."""
|
|
115
|
+
return self._no_ansi
|
|
116
|
+
|
|
117
|
+
@no_ansi.setter
|
|
118
|
+
def no_ansi(self, new_no_ansi: bool) -> None:
|
|
119
|
+
self._no_ansi = new_no_ansi
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def token_expiration_date(self) -> str | None:
|
|
123
|
+
"""When the most recently requested api_token will expire."""
|
|
124
|
+
return self._token_expiration_date
|
|
125
|
+
|
|
126
|
+
@token_expiration_date.setter
|
|
127
|
+
@persistent
|
|
128
|
+
def token_expiration_date(self, new_token_expiration_date: str) -> None:
|
|
129
|
+
self._token_expiration_date = new_token_expiration_date
|
|
130
|
+
|
|
131
|
+
def load_from_file(self, file: Path) -> None:
|
|
132
|
+
"""Load the given config file into a dict. Aborts execution if it can't decode the file.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
file: Path to the JSON config file
|
|
136
|
+
"""
|
|
137
|
+
try:
|
|
138
|
+
self._config_file = json.load(file.open("rb"))
|
|
139
|
+
except json.JSONDecodeError:
|
|
140
|
+
print.json(
|
|
141
|
+
"There was a problem loading the config file, please ensure it's valid JSON",
|
|
142
|
+
success=False,
|
|
143
|
+
path=str(file),
|
|
144
|
+
)
|
|
145
|
+
raise typer.Abort()
|
|
146
|
+
|
|
147
|
+
self._config_file_path = file
|
|
148
|
+
|
|
149
|
+
# Get all properties from the class that start with a single underscore,
|
|
150
|
+
# and initialize it with the corresponding key from the config dictionary.
|
|
151
|
+
# This generator assumes the config-file key and property will have the same name (minus the `_`)
|
|
152
|
+
properties = [
|
|
153
|
+
property
|
|
154
|
+
for property in dir(self)
|
|
155
|
+
if property not in ("_config_file", "_config_file_path")
|
|
156
|
+
and not callable(getattr(self, property))
|
|
157
|
+
and not property.startswith("__")
|
|
158
|
+
and property.startswith("_")
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
for property in properties:
|
|
162
|
+
json_var = property[1:]
|
|
163
|
+
if config_value := self._config_file.get(json_var):
|
|
164
|
+
setattr(self, property, config_value)
|
|
165
|
+
|
|
166
|
+
def print_config(self) -> None:
|
|
167
|
+
"""Print the currently loaded configuration."""
|
|
168
|
+
print.json(message=None, config_file=self._config_file, path=str(self._config_file_path))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# CLIContext pseudo-singleton instance
|
|
172
|
+
context: CLIContext = CLIContext()
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from canvas_cli.utils.context import CLIContext
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CLIContextTestHelper(CLIContext):
|
|
12
|
+
"""CLIContext subclass that defines some properties we can test."""
|
|
13
|
+
|
|
14
|
+
_persistent_mock_property: str | None = None
|
|
15
|
+
_transient_mock_property: bool = False
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def persistent_mock_property(self) -> str | None:
|
|
19
|
+
"""Mock persistent property."""
|
|
20
|
+
return self._persistent_mock_property
|
|
21
|
+
|
|
22
|
+
@persistent_mock_property.setter
|
|
23
|
+
@CLIContext.persistent
|
|
24
|
+
def persistent_mock_property(self, new_persistent_mock_property: str | None) -> None:
|
|
25
|
+
self._persistent_mock_property = new_persistent_mock_property
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def transient_mock_property(self) -> bool:
|
|
29
|
+
"""Mock transient property."""
|
|
30
|
+
return self._transient_mock_property
|
|
31
|
+
|
|
32
|
+
@transient_mock_property.setter
|
|
33
|
+
def transient_mock_property(self, new_transient_mock_property: bool) -> None:
|
|
34
|
+
self._transient_mock_property = new_transient_mock_property
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.fixture
|
|
38
|
+
def config_file(tmp_path: Path) -> Path:
|
|
39
|
+
"""Fixture that yields an empty config file and cleans up after itself."""
|
|
40
|
+
config_file = tmp_path / "mock_config.json"
|
|
41
|
+
yield config_file
|
|
42
|
+
os.remove(config_file)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.fixture
|
|
46
|
+
def valid_mock_config_file(config_file: Path) -> Path:
|
|
47
|
+
"""Fixture that yields a valid mock config file."""
|
|
48
|
+
mock_config = {
|
|
49
|
+
"persistent_mock_property": "mock-value",
|
|
50
|
+
}
|
|
51
|
+
with open(config_file, "w") as f:
|
|
52
|
+
json.dump(mock_config, f)
|
|
53
|
+
|
|
54
|
+
yield config_file
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.fixture
|
|
58
|
+
def invalid_json_mock_config_file(config_file: Path) -> Path:
|
|
59
|
+
"""Fixture that yields an invalid config file."""
|
|
60
|
+
config_file.write_text("Absolutely invalid JSON")
|
|
61
|
+
|
|
62
|
+
yield config_file
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.fixture
|
|
66
|
+
def invalid_properties_mock_config_file(config_file: Path) -> Path:
|
|
67
|
+
"""Fixture that yields a valid mock config file."""
|
|
68
|
+
mock_config = {
|
|
69
|
+
"unknown-property": "mock-value",
|
|
70
|
+
}
|
|
71
|
+
with open(config_file, "w") as f:
|
|
72
|
+
json.dump(mock_config, f)
|
|
73
|
+
|
|
74
|
+
yield config_file
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_valid_load_from_file(valid_mock_config_file: Path) -> None:
|
|
78
|
+
"""Test loading a valid config file."""
|
|
79
|
+
context = CLIContextTestHelper()
|
|
80
|
+
context.load_from_file(valid_mock_config_file)
|
|
81
|
+
|
|
82
|
+
assert context.persistent_mock_property == "mock-value"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_invalid_load_from_file(invalid_json_mock_config_file: Path) -> None:
|
|
86
|
+
"""Test loading an invalid config file aborts the execution."""
|
|
87
|
+
context = CLIContextTestHelper()
|
|
88
|
+
|
|
89
|
+
with pytest.raises(typer.Abort):
|
|
90
|
+
context.load_from_file(invalid_json_mock_config_file)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_load_invalid_property(invalid_properties_mock_config_file: Path) -> None:
|
|
94
|
+
"""Since we dynamically load the properties, this test ensures that unknown properties don't throw exceptions."""
|
|
95
|
+
context = CLIContextTestHelper()
|
|
96
|
+
|
|
97
|
+
context.load_from_file(invalid_properties_mock_config_file)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_config_persistence(valid_mock_config_file: Path) -> None:
|
|
101
|
+
"""Test marking a property with @persistent stores the value in the config file."""
|
|
102
|
+
context = CLIContextTestHelper()
|
|
103
|
+
context.load_from_file(valid_mock_config_file)
|
|
104
|
+
|
|
105
|
+
assert context.persistent_mock_property == "mock-value"
|
|
106
|
+
|
|
107
|
+
context.persistent_mock_property = "new-value"
|
|
108
|
+
|
|
109
|
+
# This won't ever happen but since the values are in memory, we need to create a new context instance,
|
|
110
|
+
# as if mimicking a new program launch
|
|
111
|
+
context_b = CLIContextTestHelper()
|
|
112
|
+
context_b.load_from_file(valid_mock_config_file)
|
|
113
|
+
|
|
114
|
+
assert context_b.persistent_mock_property == "new-value"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_config_transience(valid_mock_config_file: Path) -> None:
|
|
118
|
+
"""Test the properties transient default."""
|
|
119
|
+
context = CLIContextTestHelper()
|
|
120
|
+
context.load_from_file(valid_mock_config_file)
|
|
121
|
+
|
|
122
|
+
assert context.transient_mock_property is False
|
|
123
|
+
context.transient_mock_property = True
|
|
124
|
+
|
|
125
|
+
# This won't ever happen but since the values are in memory, we need to create a new context instance,
|
|
126
|
+
# as if mimicking a new program launch
|
|
127
|
+
context_b = CLIContextTestHelper()
|
|
128
|
+
context_b.load_from_file(valid_mock_config_file)
|
|
129
|
+
|
|
130
|
+
assert context_b.transient_mock_property is False
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from builtins import print as builtin_print
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from requests import Response
|
|
6
|
+
from rich import print as rich_print
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Printer:
|
|
10
|
+
"""Class to override Python's default print with json and Requests.response capabilities."""
|
|
11
|
+
|
|
12
|
+
def __call__(self, *args: Any) -> None:
|
|
13
|
+
"""Default printing."""
|
|
14
|
+
self._default_print(*args)
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def response(response: Response, success: bool = True) -> None:
|
|
18
|
+
"""Print a response object by getting its json or text response."""
|
|
19
|
+
try:
|
|
20
|
+
message = response.json()
|
|
21
|
+
except json.JSONDecodeError:
|
|
22
|
+
message = response.text
|
|
23
|
+
|
|
24
|
+
Printer.json(message=message, success=success, status_code=response.status_code)
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def json(message: str | list[str] | dict | None, success: bool = True, **kwargs: Any) -> None:
|
|
28
|
+
"""Print a message in json format regardless of the input."""
|
|
29
|
+
status = {"success": success}
|
|
30
|
+
if message:
|
|
31
|
+
try:
|
|
32
|
+
json_message = json.loads(message)
|
|
33
|
+
except (json.JSONDecodeError, TypeError):
|
|
34
|
+
json_message = message
|
|
35
|
+
|
|
36
|
+
status.update({"message": json_message})
|
|
37
|
+
for key, value in kwargs.items():
|
|
38
|
+
status.update({key: value})
|
|
39
|
+
|
|
40
|
+
Printer._default_print(json.dumps(status))
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def verbose(*args: Any) -> None:
|
|
44
|
+
"""Print text only if `verbose` is set in context."""
|
|
45
|
+
from canvas_cli.utils.context import context
|
|
46
|
+
|
|
47
|
+
if context.verbose:
|
|
48
|
+
Printer._default_print(*args)
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def _default_print(*args: Any) -> None:
|
|
52
|
+
from canvas_cli.utils.context import context
|
|
53
|
+
|
|
54
|
+
if context.no_ansi:
|
|
55
|
+
builtin_print(*args)
|
|
56
|
+
else:
|
|
57
|
+
rich_print(*args)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
print = Printer()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from unittest.mock import Mock
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from requests import Response
|
|
6
|
+
|
|
7
|
+
from canvas_cli.utils.print import print
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.mark.parametrize(
|
|
11
|
+
"message", ["a simple message", ["an array", "of messages"], {"one": "test"}]
|
|
12
|
+
)
|
|
13
|
+
def test_print_json_outputs_valid_json(
|
|
14
|
+
message: str | list[str] | dict | None, capfd: pytest.CaptureFixture[str]
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Test the output of print is always valid json."""
|
|
17
|
+
print.json(message)
|
|
18
|
+
output, _ = capfd.readouterr()
|
|
19
|
+
try:
|
|
20
|
+
json.loads(output)
|
|
21
|
+
except ValueError as exc:
|
|
22
|
+
assert False, f"{output} is not valid json: {exc}"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_print_json_outputs_kwargs(capfd: pytest.CaptureFixture[str]) -> None:
|
|
26
|
+
"""Test the output of print contains all given kwargs."""
|
|
27
|
+
print.json("A message", status_code=200, a_string="a_value", an_array=[1, 2], a_dict={"one": 2})
|
|
28
|
+
output, _ = capfd.readouterr()
|
|
29
|
+
try:
|
|
30
|
+
json_dict = json.loads(output)
|
|
31
|
+
|
|
32
|
+
assert json_dict.get("a_string") == "a_value"
|
|
33
|
+
assert json_dict.get("an_array") == [1, 2]
|
|
34
|
+
assert json_dict.get("a_dict") == {"one": 2}
|
|
35
|
+
|
|
36
|
+
except ValueError as exc:
|
|
37
|
+
assert False, f"{output} is not valid json: {exc}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_print_overrides_default(capfd: pytest.CaptureFixture[str]) -> None:
|
|
41
|
+
"""Test using `print` defaults to Rich."""
|
|
42
|
+
message = "Testing print"
|
|
43
|
+
print(message)
|
|
44
|
+
output, _ = capfd.readouterr()
|
|
45
|
+
assert message + "\n" == output
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_print_response_non_json_text(capfd: pytest.CaptureFixture[str]) -> None:
|
|
49
|
+
"""Test print.response with a non-json response."""
|
|
50
|
+
response = Mock(spec=Response)
|
|
51
|
+
response.status_code = 200
|
|
52
|
+
response.text = "testing text"
|
|
53
|
+
response.json.side_effect = json.JSONDecodeError("", "", 0)
|
|
54
|
+
print.response(response)
|
|
55
|
+
output, _ = capfd.readouterr()
|
|
56
|
+
assert json.loads(output) == json.loads(
|
|
57
|
+
'{"status_code": 200, "success": true, "message": "testing text"}'
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_print_response_json_text(capfd: pytest.CaptureFixture[str]) -> None:
|
|
62
|
+
"""Test print.response with a json response."""
|
|
63
|
+
response = Mock(spec=Response)
|
|
64
|
+
response.status_code = 201
|
|
65
|
+
response.json.return_value = {"something": True}
|
|
66
|
+
print.response(response)
|
|
67
|
+
output, _ = capfd.readouterr()
|
|
68
|
+
assert json.loads(output) == json.loads(
|
|
69
|
+
'{"status_code": 201, "success": true, "message": {"something": true} }'
|
|
70
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from canvas_cli.utils.urls import CoreEndpoint
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest.mark.parametrize("path", ["a-path", "/a-path", "/a-path/", "a-path/"])
|
|
7
|
+
def test_endpoint_builder(path: str) -> None:
|
|
8
|
+
"""Test that the endpoint is always generated with a trailing `/`."""
|
|
9
|
+
assert (
|
|
10
|
+
CoreEndpoint.LOG.build("https://test-host.com", path)
|
|
11
|
+
== "https://test-host.com/core/api/v1/logging/a-path/"
|
|
12
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from urllib.parse import urljoin
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class EndpointBuilderMixin:
|
|
5
|
+
"""Class that adds a url builder method."""
|
|
6
|
+
|
|
7
|
+
_base_url: str = "core/api/v1"
|
|
8
|
+
|
|
9
|
+
def __init__(self, value: str) -> None:
|
|
10
|
+
self.value = value
|
|
11
|
+
|
|
12
|
+
def build(self, host: str, *paths: str) -> str:
|
|
13
|
+
"""Builds a url from a host and a sequence of paths.
|
|
14
|
+
Assumes this is injected into an enum subclass.
|
|
15
|
+
"""
|
|
16
|
+
join = "/".join([self._base_url, self.value, "/".join(paths or [])])
|
|
17
|
+
join = join if join.endswith("/") else join + "/"
|
|
18
|
+
return urljoin(host, join)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CoreEndpoint:
|
|
22
|
+
"""Class that defines and is able to build endpoints for the /core/ path."""
|
|
23
|
+
|
|
24
|
+
_base_url: str = "core/api/v1"
|
|
25
|
+
|
|
26
|
+
PLUGIN = EndpointBuilderMixin("plugins")
|
|
27
|
+
LOG = EndpointBuilderMixin("logging")
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
manifest_schema = {
|
|
2
|
+
"type": "object",
|
|
3
|
+
"properties": {
|
|
4
|
+
"sdk_version": {"type": "string"},
|
|
5
|
+
"plugin_version": {"type": "string"},
|
|
6
|
+
"name": {"type": "string"},
|
|
7
|
+
"description": {"type": "string"},
|
|
8
|
+
"secrets": {"type": "array", "items": {"type": "string"}},
|
|
9
|
+
"components": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"properties": {
|
|
12
|
+
"commands": {"$ref": "#/$defs/component"},
|
|
13
|
+
"protocols": {"$ref": "#/$defs/component"},
|
|
14
|
+
"content": {"$ref": "#/$defs/component"},
|
|
15
|
+
"effects": {"$ref": "#/$defs/component"},
|
|
16
|
+
"views": {"$ref": "#/$defs/component"},
|
|
17
|
+
},
|
|
18
|
+
"additionalProperties": False,
|
|
19
|
+
"minProperties": 1,
|
|
20
|
+
},
|
|
21
|
+
"tags": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"properties": {
|
|
24
|
+
"patient_sourcing_and_intake": {
|
|
25
|
+
"type": "array",
|
|
26
|
+
"items": {"enum": ["symptom_triage", "coverage_capture"]},
|
|
27
|
+
},
|
|
28
|
+
"interaction_modes_and_utilization": {
|
|
29
|
+
"type": "array",
|
|
30
|
+
"items": {"enum": ["supply_policies", "demand_policies", "auto_followup"]},
|
|
31
|
+
},
|
|
32
|
+
"diagnostic_range_and_inputs": {"type": "array", "items": {"enum": []}},
|
|
33
|
+
"pricing_and_payments": {"type": "array", "items": {"enum": []}},
|
|
34
|
+
"care_team_composition": {"type": "array", "items": {"enum": []}},
|
|
35
|
+
"interventions_and_safety": {"type": "array", "items": {"enum": []}},
|
|
36
|
+
"content": {"type": "array", "items": {"enum": ["patient_intake"]}},
|
|
37
|
+
},
|
|
38
|
+
"additionalProperties": False,
|
|
39
|
+
},
|
|
40
|
+
"references": {"type": "array", "items": {"type": "string"}},
|
|
41
|
+
"license": {"type": "string"},
|
|
42
|
+
"diagram": {"type": ["boolean", "string"]},
|
|
43
|
+
"readme": {"type": ["boolean", "string"]},
|
|
44
|
+
},
|
|
45
|
+
"required": [
|
|
46
|
+
"sdk_version",
|
|
47
|
+
"plugin_version",
|
|
48
|
+
"name",
|
|
49
|
+
"description",
|
|
50
|
+
"components",
|
|
51
|
+
"tags",
|
|
52
|
+
"license",
|
|
53
|
+
"readme",
|
|
54
|
+
],
|
|
55
|
+
"additionalProperties": False,
|
|
56
|
+
"$defs": {
|
|
57
|
+
"component": {
|
|
58
|
+
"type": "array",
|
|
59
|
+
"items": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"properties": {
|
|
62
|
+
"class": {"type": "string"},
|
|
63
|
+
"description": {"type": "string"},
|
|
64
|
+
"data_access": {
|
|
65
|
+
"type": "object",
|
|
66
|
+
"properties": {
|
|
67
|
+
"event": {"type": "string"},
|
|
68
|
+
"read": {"type": "array", "items": {"type": "string"}},
|
|
69
|
+
"write": {"type": "array", "items": {"type": "string"}},
|
|
70
|
+
},
|
|
71
|
+
"required": ["event", "read", "write"],
|
|
72
|
+
"additionalProperties": False,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
"required": ["class", "description", "data_access"],
|
|
76
|
+
"additionalProperties": False,
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from canvas_cli.utils.validators import validate_manifest_file
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest.fixture
|
|
7
|
+
def protocol_manifest_example() -> dict:
|
|
8
|
+
return {
|
|
9
|
+
"sdk_version": "0.3.1",
|
|
10
|
+
"plugin_version": "1.0.1",
|
|
11
|
+
"name": "Prompt to prescribe when assessing condition",
|
|
12
|
+
"description": "To assist in ....",
|
|
13
|
+
"components": {
|
|
14
|
+
"protocols": [
|
|
15
|
+
{
|
|
16
|
+
"class": "prompt_to_prescribe.protocols.prompt_when_assessing.PromptWhenAssessing",
|
|
17
|
+
"description": "probably the same as the plugin's description",
|
|
18
|
+
"data_access": {
|
|
19
|
+
"event": "",
|
|
20
|
+
"read": ["conditions"],
|
|
21
|
+
"write": ["commands"],
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"tags": {"patient_sourcing_and_intake": ["symptom_triage"]},
|
|
27
|
+
"references": [],
|
|
28
|
+
"license": "",
|
|
29
|
+
"diagram": False,
|
|
30
|
+
"readme": "README.MD",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_manifest_file_schema(protocol_manifest_example: dict) -> None:
|
|
35
|
+
"""Test that no exception raised when a valid manifest file is validated"""
|
|
36
|
+
validate_manifest_file(protocol_manifest_example)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from jsonschema import validators
|
|
2
|
+
|
|
3
|
+
from canvas_cli.utils.context import context
|
|
4
|
+
from canvas_cli.utils.print import print
|
|
5
|
+
from canvas_cli.utils.validators.manifest_schema import manifest_schema
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_default_host(host: str | None) -> str | None:
|
|
9
|
+
"""Return context's default host if the host param is null."""
|
|
10
|
+
return host or context.default_host # type: ignore
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def validate_manifest_file(manifest_json: dict) -> None:
|
|
14
|
+
"""Validates a Canvas Manifest json against the manifest schema."""
|
|
15
|
+
validator = validators.validator_for(manifest_schema)(manifest_schema)
|
|
16
|
+
tag_warnings = []
|
|
17
|
+
tag_value_warnings = []
|
|
18
|
+
other_warnings = []
|
|
19
|
+
for error in validator.iter_errors(manifest_json):
|
|
20
|
+
if error.path and len(error.path) and error.path[0] == "tags":
|
|
21
|
+
if error.validator == "enum":
|
|
22
|
+
tag_value_warnings.append(error)
|
|
23
|
+
elif error.validator == "additionalProperties":
|
|
24
|
+
tag_warnings.append(error)
|
|
25
|
+
else:
|
|
26
|
+
other_warnings.append(error)
|
|
27
|
+
else:
|
|
28
|
+
raise error
|
|
29
|
+
|
|
30
|
+
if tag_warnings:
|
|
31
|
+
print("Warning: there are unrecognized tags in the manifest file:")
|
|
32
|
+
for tag_warning in tag_warnings:
|
|
33
|
+
print(f"\t- {tag_warning.message}")
|
|
34
|
+
if tag_value_warnings:
|
|
35
|
+
print("Warning: there are unrecognized tag values in the manifest file:")
|
|
36
|
+
for tag_value_warning in tag_value_warnings:
|
|
37
|
+
tag_category = tag_value_warning.path[1]
|
|
38
|
+
print(
|
|
39
|
+
f"\t- Please choose a valid tag value for '{tag_category}'. {tag_value_warning.message}."
|
|
40
|
+
)
|
canvas_sdk/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from canvas_sdk.commands.commands.assess import AssessCommand
|
|
2
|
+
from canvas_sdk.commands.commands.diagnose import DiagnoseCommand
|
|
3
|
+
from canvas_sdk.commands.commands.goal import GoalCommand
|
|
4
|
+
from canvas_sdk.commands.commands.history_present_illness import (
|
|
5
|
+
HistoryOfPresentIllnessCommand,
|
|
6
|
+
)
|
|
7
|
+
from canvas_sdk.commands.commands.medication_statement import MedicationStatementCommand
|
|
8
|
+
from canvas_sdk.commands.commands.plan import PlanCommand
|
|
9
|
+
from canvas_sdk.commands.commands.prescribe import PrescribeCommand
|
|
10
|
+
from canvas_sdk.commands.commands.questionnaire import QuestionnaireCommand
|
|
11
|
+
from canvas_sdk.commands.commands.reason_for_visit import ReasonForVisitCommand
|
|
12
|
+
from canvas_sdk.commands.commands.stop_medication import StopMedicationCommand
|
|
13
|
+
from canvas_sdk.commands.commands.update_goal import UpdateGoalCommand
|
|
14
|
+
|
|
15
|
+
__all__ = (
|
|
16
|
+
"AssessCommand",
|
|
17
|
+
"DiagnoseCommand",
|
|
18
|
+
"GoalCommand",
|
|
19
|
+
"HistoryOfPresentIllnessCommand",
|
|
20
|
+
"MedicationStatementCommand",
|
|
21
|
+
"PlanCommand",
|
|
22
|
+
"PrescribeCommand",
|
|
23
|
+
"QuestionnaireCommand",
|
|
24
|
+
"ReasonForVisitCommand",
|
|
25
|
+
"StopMedicationCommand",
|
|
26
|
+
"UpdateGoalCommand",
|
|
27
|
+
)
|