canvas 0.1.3__py3-none-any.whl → 0.1.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of canvas might be problematic. Click here for more details.

Files changed (70) hide show
  1. canvas-0.1.5.dist-info/METADATA +176 -0
  2. canvas-0.1.5.dist-info/RECORD +66 -0
  3. {canvas-0.1.3.dist-info → canvas-0.1.5.dist-info}/WHEEL +1 -1
  4. canvas-0.1.5.dist-info/entry_points.txt +3 -0
  5. canvas_cli/apps/__init__.py +0 -0
  6. canvas_cli/apps/auth/__init__.py +3 -0
  7. canvas_cli/apps/auth/tests.py +142 -0
  8. canvas_cli/apps/auth/utils.py +163 -0
  9. canvas_cli/apps/logs/__init__.py +3 -0
  10. canvas_cli/apps/logs/logs.py +59 -0
  11. canvas_cli/apps/plugin/__init__.py +9 -0
  12. canvas_cli/apps/plugin/plugin.py +286 -0
  13. canvas_cli/apps/plugin/tests.py +32 -0
  14. canvas_cli/conftest.py +28 -0
  15. canvas_cli/main.py +78 -0
  16. canvas_cli/templates/plugins/default/cookiecutter.json +4 -0
  17. canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/CANVAS_MANIFEST.json +29 -0
  18. canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/README.md +12 -0
  19. canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/protocols/__init__.py +0 -0
  20. canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/protocols/my_protocol.py +55 -0
  21. canvas_cli/tests.py +11 -0
  22. canvas_cli/utils/__init__.py +0 -0
  23. canvas_cli/utils/context/__init__.py +3 -0
  24. canvas_cli/utils/context/context.py +172 -0
  25. canvas_cli/utils/context/tests.py +130 -0
  26. canvas_cli/utils/print/__init__.py +3 -0
  27. canvas_cli/utils/print/print.py +60 -0
  28. canvas_cli/utils/print/tests.py +70 -0
  29. canvas_cli/utils/urls/__init__.py +3 -0
  30. canvas_cli/utils/urls/tests.py +12 -0
  31. canvas_cli/utils/urls/urls.py +27 -0
  32. canvas_cli/utils/validators/__init__.py +3 -0
  33. canvas_cli/utils/validators/manifest_schema.py +80 -0
  34. canvas_cli/utils/validators/tests.py +36 -0
  35. canvas_cli/utils/validators/validators.py +40 -0
  36. canvas_sdk/__init__.py +0 -0
  37. canvas_sdk/commands/__init__.py +27 -0
  38. canvas_sdk/commands/base.py +118 -0
  39. canvas_sdk/commands/commands/assess.py +48 -0
  40. canvas_sdk/commands/commands/diagnose.py +44 -0
  41. canvas_sdk/commands/commands/goal.py +48 -0
  42. canvas_sdk/commands/commands/history_present_illness.py +15 -0
  43. canvas_sdk/commands/commands/medication_statement.py +28 -0
  44. canvas_sdk/commands/commands/plan.py +15 -0
  45. canvas_sdk/commands/commands/prescribe.py +48 -0
  46. canvas_sdk/commands/commands/questionnaire.py +17 -0
  47. canvas_sdk/commands/commands/reason_for_visit.py +36 -0
  48. canvas_sdk/commands/commands/stop_medication.py +18 -0
  49. canvas_sdk/commands/commands/update_goal.py +48 -0
  50. canvas_sdk/commands/constants.py +9 -0
  51. canvas_sdk/commands/tests/test_utils.py +195 -0
  52. canvas_sdk/commands/tests/tests.py +407 -0
  53. canvas_sdk/data/__init__.py +0 -0
  54. canvas_sdk/effects/__init__.py +1 -0
  55. canvas_sdk/effects/banner_alert/banner_alert.py +37 -0
  56. canvas_sdk/effects/banner_alert/constants.py +19 -0
  57. canvas_sdk/effects/base.py +30 -0
  58. canvas_sdk/events/__init__.py +1 -0
  59. canvas_sdk/protocols/__init__.py +1 -0
  60. canvas_sdk/protocols/base.py +12 -0
  61. canvas_sdk/tests/__init__.py +0 -0
  62. canvas_sdk/utils/__init__.py +3 -0
  63. canvas_sdk/utils/http.py +72 -0
  64. canvas_sdk/utils/tests.py +63 -0
  65. canvas_sdk/views/__init__.py +0 -0
  66. canvas/main.py +0 -19
  67. canvas-0.1.3.dist-info/METADATA +0 -285
  68. canvas-0.1.3.dist-info/RECORD +0 -6
  69. canvas-0.1.3.dist-info/entry_points.txt +0 -3
  70. {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,3 @@
1
+ from canvas_cli.utils.print.print import Printer, print
2
+
3
+ __all__ = ("print", "Printer")
@@ -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,3 @@
1
+ from canvas_cli.utils.urls.urls import CoreEndpoint
2
+
3
+ __all__ = ("CoreEndpoint",)
@@ -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,3 @@
1
+ from canvas_cli.utils.validators.validators import validate_manifest_file
2
+
3
+ __all__ = ("validate_manifest_file",)
@@ -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
+ )