canvas 0.14.0__py3-none-any.whl → 0.16.0__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 (88) hide show
  1. {canvas-0.14.0.dist-info → canvas-0.16.0.dist-info}/METADATA +2 -2
  2. {canvas-0.14.0.dist-info → canvas-0.16.0.dist-info}/RECORD +87 -52
  3. canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/CANVAS_MANIFEST.json +6 -3
  4. canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/applications/my_application.py +4 -1
  5. canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/protocols/my_protocol.py +1 -1
  6. canvas_cli/utils/validators/manifest_schema.py +9 -2
  7. canvas_generated/messages/effects_pb2.py +2 -2
  8. canvas_generated/messages/effects_pb2.pyi +18 -0
  9. canvas_generated/messages/events_pb2.py +2 -2
  10. canvas_generated/messages/events_pb2.pyi +48 -0
  11. canvas_sdk/commands/tests/protocol/tests.py +3 -1
  12. canvas_sdk/commands/tests/test_utils.py +76 -18
  13. canvas_sdk/effects/banner_alert/tests.py +41 -20
  14. canvas_sdk/effects/launch_modal.py +14 -3
  15. canvas_sdk/events/base.py +1 -3
  16. canvas_sdk/handlers/action_button.py +33 -13
  17. canvas_sdk/handlers/application.py +1 -1
  18. canvas_sdk/handlers/cron_task.py +1 -1
  19. canvas_sdk/protocols/clinical_quality_measure.py +1 -1
  20. canvas_sdk/templates/__init__.py +3 -0
  21. canvas_sdk/templates/tests/__init__.py +0 -0
  22. canvas_sdk/templates/tests/test_utils.py +43 -0
  23. canvas_sdk/templates/utils.py +44 -0
  24. canvas_sdk/v1/apps.py +7 -0
  25. canvas_sdk/v1/data/__init__.py +98 -5
  26. canvas_sdk/v1/data/allergy_intolerance.py +25 -9
  27. canvas_sdk/v1/data/appointment.py +56 -0
  28. canvas_sdk/v1/data/assessment.py +40 -0
  29. canvas_sdk/v1/data/base.py +35 -22
  30. canvas_sdk/v1/data/billing.py +4 -7
  31. canvas_sdk/v1/data/care_team.py +60 -0
  32. canvas_sdk/v1/data/command.py +8 -10
  33. canvas_sdk/v1/data/common.py +53 -0
  34. canvas_sdk/v1/data/condition.py +22 -10
  35. canvas_sdk/v1/data/coverage.py +294 -0
  36. canvas_sdk/v1/data/detected_issue.py +5 -9
  37. canvas_sdk/v1/data/device.py +4 -8
  38. canvas_sdk/v1/data/imaging.py +12 -17
  39. canvas_sdk/v1/data/lab.py +41 -31
  40. canvas_sdk/v1/data/medication.py +16 -10
  41. canvas_sdk/v1/data/note.py +11 -14
  42. canvas_sdk/v1/data/observation.py +19 -14
  43. canvas_sdk/v1/data/organization.py +1 -2
  44. canvas_sdk/v1/data/patient.py +140 -2
  45. canvas_sdk/v1/data/practicelocation.py +2 -4
  46. canvas_sdk/v1/data/protocol_override.py +21 -8
  47. canvas_sdk/v1/data/questionnaire.py +20 -17
  48. canvas_sdk/v1/data/staff.py +5 -7
  49. canvas_sdk/v1/data/task.py +5 -11
  50. canvas_sdk/v1/data/user.py +0 -1
  51. canvas_sdk/v1/models.py +4 -0
  52. canvas_sdk/value_set/hcc2018.py +55369 -0
  53. plugin_runner/plugin_installer.py +1 -1
  54. plugin_runner/plugin_runner.py +5 -25
  55. plugin_runner/sandbox.py +133 -9
  56. plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/CANVAS_MANIFEST.json +38 -0
  57. plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/README.md +11 -0
  58. plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/protocols/__init__.py +0 -0
  59. plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/protocols/my_protocol.py +33 -0
  60. plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/templates/__init__.py +3 -0
  61. plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/templates/base.py +6 -0
  62. plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/utils/__init__.py +5 -0
  63. plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/utils/base.py +4 -0
  64. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/CANVAS_MANIFEST.json +29 -0
  65. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/README.md +12 -0
  66. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/other_module/__init__.py +0 -0
  67. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/other_module/base.py +10 -0
  68. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/protocols/__init__.py +0 -0
  69. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/protocols/my_protocol.py +18 -0
  70. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/CANVAS_MANIFEST.json +29 -0
  71. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/README.md +12 -0
  72. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/other_module/__init__.py +0 -0
  73. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/other_module/base.py +10 -0
  74. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/protocols/__init__.py +0 -0
  75. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/protocols/my_protocol.py +18 -0
  76. plugin_runner/tests/fixtures/plugins/test_render_template/CANVAS_MANIFEST.json +47 -0
  77. plugin_runner/tests/fixtures/plugins/test_render_template/README.md +11 -0
  78. plugin_runner/tests/fixtures/plugins/test_render_template/protocols/__init__.py +0 -0
  79. plugin_runner/tests/fixtures/plugins/test_render_template/protocols/my_protocol.py +43 -0
  80. plugin_runner/tests/fixtures/plugins/test_render_template/templates/template.html +10 -0
  81. plugin_runner/tests/test_application.py +9 -9
  82. plugin_runner/tests/test_plugin_installer.py +12 -1
  83. plugin_runner/tests/test_plugin_runner.py +159 -66
  84. plugin_runner/tests/test_sandbox.py +26 -14
  85. settings.py +13 -1
  86. canvas_sdk/models/__init__.py +0 -8
  87. {canvas-0.14.0.dist-info → canvas-0.16.0.dist-info}/WHEEL +0 -0
  88. {canvas-0.14.0.dist-info → canvas-0.16.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,43 @@
1
+ from canvas_sdk.effects import Effect, EffectType
2
+ from canvas_sdk.events import EventType
3
+ from canvas_sdk.protocols import BaseProtocol
4
+ from canvas_sdk.templates import render_to_string
5
+
6
+
7
+ class ValidTemplate(BaseProtocol):
8
+ """You should put a helpful description of this protocol's behavior here."""
9
+
10
+ RESPONDS_TO = [EventType.Name(EventType.UNKNOWN)]
11
+
12
+ def compute(self) -> list[Effect]:
13
+ """This method gets called when an event of the type RESPONDS_TO is fired."""
14
+ return [
15
+ Effect(type=EffectType.LOG, payload=render_to_string("templates/template.html", None))
16
+ ]
17
+
18
+
19
+ class InvalidTemplate(BaseProtocol):
20
+ """You should put a helpful description of this protocol's behavior here."""
21
+
22
+ RESPONDS_TO = [EventType.Name(EventType.UNKNOWN)]
23
+
24
+ def compute(self) -> list[Effect]:
25
+ """This method gets called when an event of the type RESPONDS_TO is fired."""
26
+ return [
27
+ Effect(type=EffectType.LOG, payload=render_to_string("templates/template1.html", None))
28
+ ]
29
+
30
+
31
+ class ForbiddenTemplate(BaseProtocol):
32
+ """You should put a helpful description of this protocol's behavior here."""
33
+
34
+ RESPONDS_TO = [EventType.Name(EventType.UNKNOWN)]
35
+
36
+ def compute(self) -> list[Effect]:
37
+ """This method gets called when an event of the type RESPONDS_TO is fired."""
38
+ return [
39
+ Effect(
40
+ type=EffectType.LOG,
41
+ payload=render_to_string("../../templates/template.html", None),
42
+ )
43
+ ]
@@ -0,0 +1,10 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Title</title>
6
+ </head>
7
+ <body>
8
+
9
+ </body>
10
+ </html>
@@ -6,7 +6,7 @@ from canvas_sdk.events import Event, EventRequest, EventType
6
6
  from canvas_sdk.handlers.application import Application
7
7
 
8
8
 
9
- class TestApplication(Application):
9
+ class ExampleApplication(Application):
10
10
  """A concrete implementation of the Application class for testing."""
11
11
 
12
12
  def on_open(self) -> Effect:
@@ -15,9 +15,9 @@ class TestApplication(Application):
15
15
 
16
16
 
17
17
  @pytest.fixture
18
- def app_instance(event: Event) -> TestApplication:
18
+ def app_instance(event: Event) -> ExampleApplication:
19
19
  """Provide an instance of the TestApplication with a mocked event."""
20
- app = TestApplication(event)
20
+ app = ExampleApplication(event)
21
21
  return app
22
22
 
23
23
 
@@ -25,7 +25,7 @@ def test_compute_event_not_targeted() -> None:
25
25
  """Test that compute filters out events not targeted for the app."""
26
26
  request = EventRequest(type=EventType.APPLICATION__ON_OPEN, target="some_identifier")
27
27
  event = Event(request)
28
- app = TestApplication(event)
28
+ app = ExampleApplication(event)
29
29
 
30
30
  result = app.compute()
31
31
 
@@ -36,10 +36,10 @@ def test_compute_event_targeted() -> None:
36
36
  """Test that compute processes events targeted for the app."""
37
37
  request = EventRequest(
38
38
  type=EventType.APPLICATION__ON_OPEN,
39
- target=f"{TestApplication.__module__}:{TestApplication.__qualname__}",
39
+ target=f"{ExampleApplication.__module__}:{ExampleApplication.__qualname__}",
40
40
  )
41
41
  event = Event(request)
42
- app = TestApplication(event)
42
+ app = ExampleApplication(event)
43
43
  result = app.compute()
44
44
 
45
45
  assert len(result) == 1, "Expected a single effect if the event target is the app identifier"
@@ -48,13 +48,13 @@ def test_compute_event_targeted() -> None:
48
48
 
49
49
  def test_identifier_property() -> None:
50
50
  """Test the identifier property of the Application class."""
51
- expected_identifier = f"{TestApplication.__module__}:{TestApplication.__qualname__}"
51
+ expected_identifier = f"{ExampleApplication.__module__}:{ExampleApplication.__qualname__}"
52
52
  request = EventRequest(
53
53
  type=EventType.APPLICATION__ON_OPEN,
54
- target=f"{TestApplication.__module__}:{TestApplication.__qualname__}",
54
+ target=f"{ExampleApplication.__module__}:{ExampleApplication.__qualname__}",
55
55
  )
56
56
  event = Event(request)
57
- app = TestApplication(event)
57
+ app = ExampleApplication(event)
58
58
 
59
59
  assert app.identifier == expected_identifier, "The identifier property is incorrect"
60
60
 
@@ -83,8 +83,19 @@ def test_plugin_installation_from_tarball(mocker: MockerFixture) -> None:
83
83
  tarball_2 = _create_tarball("plugin2")
84
84
 
85
85
  mocker.patch("plugin_runner.plugin_installer.enabled_plugins", return_value=mock_plugins)
86
+
87
+ def mock_download_plugin(package: str) -> MagicMock:
88
+ mock_context = mocker.Mock()
89
+ if package == "plugins/plugin1.tar.gz":
90
+ mock_context.__enter__ = mocker.Mock(return_value=tarball_1)
91
+ elif package == "plugins/plugin2.tar":
92
+ mock_context.__enter__ = mocker.Mock(return_value=tarball_2)
93
+ mock_context.__exit__ = mocker.Mock(return_value=None)
94
+ return mock_context
95
+
86
96
  mocker.patch(
87
- "plugin_runner.plugin_installer.download_plugin", side_effect=[tarball_1, tarball_2]
97
+ "plugin_runner.plugin_installer.download_plugin",
98
+ side_effect=mock_download_plugin,
88
99
  )
89
100
 
90
101
  install_plugins()
@@ -1,5 +1,5 @@
1
+ import logging
1
2
  import shutil
2
- from collections.abc import Generator
3
3
  from pathlib import Path
4
4
  from unittest.mock import MagicMock, patch
5
5
 
@@ -7,50 +7,16 @@ import pytest
7
7
 
8
8
  from canvas_generated.messages.effects_pb2 import EffectType
9
9
  from canvas_generated.messages.plugins_pb2 import ReloadPluginsRequest
10
- from canvas_sdk.events import EventRequest, EventType
10
+ from canvas_sdk.events import Event, EventRequest, EventType
11
11
  from plugin_runner.plugin_runner import (
12
12
  EVENT_HANDLER_MAP,
13
13
  LOADED_PLUGINS,
14
14
  PluginRunner,
15
+ load_or_reload_plugin,
15
16
  load_plugins,
16
- sandbox_from_package,
17
17
  )
18
18
 
19
19
 
20
- @pytest.fixture
21
- def setup_test_plugin(request: pytest.FixtureRequest) -> Generator[Path, None, None]:
22
- """Copies a specified plugin from the fixtures directory to the data directory
23
- and removes it after the test.
24
-
25
- Parameters:
26
- - request.param: The name of the plugin package to copy.
27
-
28
- Yields:
29
- - Path to the copied plugin directory.
30
- """
31
- # Define base directories
32
- base_dir = Path("./plugin_runner/tests")
33
- fixture_plugin_dir = base_dir / "fixtures" / "plugins"
34
- data_plugin_dir = base_dir / "data" / "plugins"
35
-
36
- # The plugin name should be passed as a parameter to the fixture
37
- plugin_name = request.param # Expected to be a str
38
- src_plugin_path = fixture_plugin_dir / plugin_name
39
- dest_plugin_path = data_plugin_dir / plugin_name
40
-
41
- # Ensure the data plugin directory exists
42
- data_plugin_dir.mkdir(parents=True, exist_ok=True)
43
-
44
- # Copy the specific plugin from fixtures to data
45
- try:
46
- shutil.copytree(src_plugin_path, dest_plugin_path)
47
- yield dest_plugin_path # Provide the path to the test
48
- finally:
49
- # Cleanup: remove data/plugins directory after the test
50
- if dest_plugin_path.exists():
51
- shutil.rmtree(dest_plugin_path)
52
-
53
-
54
20
  @pytest.fixture
55
21
  def plugin_runner() -> PluginRunner:
56
22
  """Fixture to initialize PluginRunner with mocks."""
@@ -59,11 +25,9 @@ def plugin_runner() -> PluginRunner:
59
25
  return runner
60
26
 
61
27
 
62
- @pytest.mark.parametrize("setup_test_plugin", ["example_plugin"], indirect=True)
63
- def test_load_plugins_with_valid_plugin(setup_test_plugin: Path) -> None:
28
+ @pytest.mark.parametrize("install_test_plugin", ["example_plugin"], indirect=True)
29
+ def test_load_plugins_with_valid_plugin(install_test_plugin: Path, load_test_plugins: None) -> None:
64
30
  """Test loading plugins with a valid plugin."""
65
- load_plugins()
66
-
67
31
  assert "example_plugin:example_plugin.protocols.my_protocol:Protocol" in LOADED_PLUGINS
68
32
  assert (
69
33
  LOADED_PLUGINS["example_plugin:example_plugin.protocols.my_protocol:Protocol"]["active"]
@@ -72,12 +36,11 @@ def test_load_plugins_with_valid_plugin(setup_test_plugin: Path) -> None:
72
36
 
73
37
 
74
38
  @pytest.mark.asyncio
75
- @pytest.mark.parametrize("setup_test_plugin", ["test_module_imports_plugin"], indirect=True)
39
+ @pytest.mark.parametrize("install_test_plugin", ["test_module_imports_plugin"], indirect=True)
76
40
  async def test_load_plugins_with_plugin_that_imports_other_modules_within_plugin_package(
77
- setup_test_plugin: Path, plugin_runner: PluginRunner
41
+ install_test_plugin: Path, plugin_runner: PluginRunner, load_test_plugins: None
78
42
  ) -> None:
79
43
  """Test loading plugins with a valid plugin that imports other modules within the current plugin package."""
80
- load_plugins()
81
44
  assert (
82
45
  "test_module_imports_plugin:test_module_imports_plugin.protocols.my_protocol:Protocol"
83
46
  in LOADED_PLUGINS
@@ -102,7 +65,7 @@ async def test_load_plugins_with_plugin_that_imports_other_modules_within_plugin
102
65
 
103
66
 
104
67
  @pytest.mark.parametrize(
105
- "setup_test_plugin",
68
+ "install_test_plugin",
106
69
  [
107
70
  "test_module_imports_outside_plugin_v1",
108
71
  "test_module_imports_outside_plugin_v2",
@@ -111,18 +74,108 @@ async def test_load_plugins_with_plugin_that_imports_other_modules_within_plugin
111
74
  indirect=True,
112
75
  )
113
76
  def test_load_plugins_with_plugin_that_imports_other_modules_outside_plugin_package(
114
- setup_test_plugin: Path,
77
+ install_test_plugin: Path, caplog: pytest.LogCaptureFixture
115
78
  ) -> None:
116
79
  """Test loading plugins with an invalid plugin that imports other modules outside the current plugin package."""
117
- with pytest.raises(ImportError, match="is not an allowed import"):
118
- sandbox_from_package(setup_test_plugin)
80
+ with caplog.at_level(logging.ERROR):
81
+ load_or_reload_plugin(install_test_plugin)
82
+
83
+ assert any(
84
+ "Error importing module" in record.message for record in caplog.records
85
+ ), "log.error() was not called with the expected message."
86
+
87
+
88
+ @pytest.mark.parametrize(
89
+ "install_test_plugin",
90
+ [
91
+ "test_module_forbidden_imports_plugin",
92
+ ],
93
+ indirect=True,
94
+ )
95
+ def test_load_plugins_with_plugin_that_imports_forbidden_modules(
96
+ install_test_plugin: Path, caplog: pytest.LogCaptureFixture
97
+ ) -> None:
98
+ """Test loading plugins with an invalid plugin that imports forbidden modules."""
99
+ with caplog.at_level(logging.ERROR):
100
+ load_or_reload_plugin(install_test_plugin)
119
101
 
102
+ assert any(
103
+ "Error importing module" in record.message for record in caplog.records
104
+ ), "log.error() was not called with the expected message."
120
105
 
121
- @pytest.mark.parametrize("setup_test_plugin", ["example_plugin"], indirect=True)
122
- def test_reload_plugin(setup_test_plugin: Path) -> None:
106
+
107
+ @pytest.mark.parametrize(
108
+ "install_test_plugin",
109
+ [
110
+ "test_module_forbidden_imports_runtime_plugin",
111
+ ],
112
+ indirect=True,
113
+ )
114
+ def test_load_plugins_with_plugin_that_imports_forbidden_modules_at_runtime(
115
+ install_test_plugin: Path,
116
+ ) -> None:
117
+ """Test loading plugins with an invalid plugin that imports forbidden modules at runtime."""
118
+ with pytest.raises(ImportError, match="is not an allowed import."):
119
+ load_or_reload_plugin(install_test_plugin)
120
+ class_handler = LOADED_PLUGINS[
121
+ "test_module_forbidden_imports_runtime_plugin:test_module_forbidden_imports_runtime_plugin.protocols.my_protocol:Protocol"
122
+ ]["class"]
123
+ class_handler(Event(EventRequest(type=EventType.UNKNOWN))).compute()
124
+
125
+
126
+ @pytest.mark.parametrize(
127
+ "install_test_plugin",
128
+ [
129
+ "test_implicit_imports_plugin",
130
+ ],
131
+ indirect=True,
132
+ )
133
+ def test_plugin_that_implicitly_imports_allowed_modules(
134
+ install_test_plugin: Path, caplog: pytest.LogCaptureFixture
135
+ ) -> None:
136
+ """Test loading plugins with a plugin that implicitly imports allowed modules."""
137
+ with caplog.at_level(logging.INFO):
138
+ load_or_reload_plugin(install_test_plugin)
139
+ class_handler = LOADED_PLUGINS[
140
+ "test_implicit_imports_plugin:test_implicit_imports_plugin.protocols.my_protocol:Allowed"
141
+ ]["class"]
142
+ class_handler(Event(EventRequest(type=EventType.UNKNOWN))).compute()
143
+
144
+ assert any(
145
+ "Hello, World!" in record.message for record in caplog.records
146
+ ), "log.info() with Template.render() was not called."
147
+
148
+
149
+ @pytest.mark.parametrize(
150
+ "install_test_plugin",
151
+ [
152
+ "test_implicit_imports_plugin",
153
+ ],
154
+ indirect=True,
155
+ )
156
+ def test_plugin_that_implicitly_imports_forbidden_modules(
157
+ install_test_plugin: Path, caplog: pytest.LogCaptureFixture
158
+ ) -> None:
159
+ """Test loading plugins with an invalid plugin that implicitly imports forbidden modules."""
160
+ with (
161
+ caplog.at_level(logging.INFO),
162
+ pytest.raises(ImportError, match="'os' is not an allowed import."),
163
+ ):
164
+ load_or_reload_plugin(install_test_plugin)
165
+ class_handler = LOADED_PLUGINS[
166
+ "test_implicit_imports_plugin:test_implicit_imports_plugin.protocols.my_protocol:Forbidden"
167
+ ]["class"]
168
+ class_handler(Event(EventRequest(type=EventType.UNKNOWN))).compute()
169
+
170
+ assert (
171
+ any("os list dir" in record.message for record in caplog.records) is False
172
+ ), "log.info() with os.listdir() was called."
173
+
174
+
175
+ @pytest.mark.parametrize("install_test_plugin", ["example_plugin"], indirect=True)
176
+ def test_reload_plugin(install_test_plugin: Path, load_test_plugins: None) -> None:
123
177
  """Test reloading a plugin."""
124
178
  load_plugins()
125
- load_plugins()
126
179
 
127
180
  assert "example_plugin:example_plugin.protocols.my_protocol:Protocol" in LOADED_PLUGINS
128
181
  assert (
@@ -131,18 +184,22 @@ def test_reload_plugin(setup_test_plugin: Path) -> None:
131
184
  )
132
185
 
133
186
 
134
- @pytest.mark.parametrize("setup_test_plugin", ["example_plugin"], indirect=True)
135
- def test_remove_plugin_should_be_removed_from_loaded_plugins(setup_test_plugin: Path) -> None:
187
+ @pytest.mark.parametrize("install_test_plugin", ["example_plugin"], indirect=True)
188
+ def test_remove_plugin_should_be_removed_from_loaded_plugins(
189
+ install_test_plugin: Path, load_test_plugins: None
190
+ ) -> None:
136
191
  """Test removing a plugin."""
137
- load_plugins()
138
192
  assert "example_plugin:example_plugin.protocols.my_protocol:Protocol" in LOADED_PLUGINS
139
- shutil.rmtree(setup_test_plugin)
193
+ shutil.rmtree(install_test_plugin)
140
194
  load_plugins()
141
195
  assert "example_plugin:example_plugin.protocols.my_protocol:Protocol" not in LOADED_PLUGINS
142
196
 
143
197
 
144
- @pytest.mark.parametrize("setup_test_plugin", ["example_plugin"], indirect=True)
145
- def test_load_plugins_should_refresh_event_protocol_map(setup_test_plugin: Path) -> None:
198
+ @pytest.mark.parametrize("install_test_plugin", ["example_plugin"], indirect=True)
199
+ @pytest.mark.parametrize("load_test_plugins", [None], indirect=True)
200
+ def test_load_plugins_should_refresh_event_protocol_map(
201
+ load_test_plugins: None, install_test_plugin: Path
202
+ ) -> None:
146
203
  """Test that the event protocol map is refreshed when loading plugins."""
147
204
  assert EVENT_HANDLER_MAP == {}
148
205
  load_plugins()
@@ -153,13 +210,11 @@ def test_load_plugins_should_refresh_event_protocol_map(setup_test_plugin: Path)
153
210
 
154
211
 
155
212
  @pytest.mark.asyncio
156
- @pytest.mark.parametrize("setup_test_plugin", ["example_plugin"], indirect=True)
213
+ @pytest.mark.parametrize("install_test_plugin", ["example_plugin"], indirect=True)
157
214
  async def test_handle_plugin_event_returns_expected_result(
158
- setup_test_plugin: Path, plugin_runner: PluginRunner
215
+ install_test_plugin: Path, plugin_runner: PluginRunner, load_test_plugins: None
159
216
  ) -> None:
160
217
  """Test that HandleEvent successfully calls the relevant plugins and returns the expected result."""
161
- load_plugins()
162
-
163
218
  event = EventRequest(type=EventType.UNKNOWN)
164
219
 
165
220
  result = []
@@ -174,11 +229,11 @@ async def test_handle_plugin_event_returns_expected_result(
174
229
 
175
230
 
176
231
  @pytest.mark.asyncio
177
- @pytest.mark.parametrize("setup_test_plugin", ["example_plugin"], indirect=True)
178
- async def test_reload_plugins_event_handler_successfully_loads_plugins(
179
- setup_test_plugin: Path, plugin_runner: PluginRunner
232
+ @pytest.mark.parametrize("install_test_plugin", ["example_plugin"], indirect=True)
233
+ async def test_reload_plugins_event_handler_successfully_publishes_message(
234
+ install_test_plugin: Path, plugin_runner: PluginRunner
180
235
  ) -> None:
181
- """Test ReloadPlugins Event handler successfully loads plugins."""
236
+ """Test ReloadPlugins Event handler successfully publishes a message with restart action."""
182
237
  with patch("plugin_runner.plugin_runner.publish_message", MagicMock()) as mock_publish_message:
183
238
  request = ReloadPluginsRequest()
184
239
 
@@ -190,4 +245,42 @@ async def test_reload_plugins_event_handler_successfully_loads_plugins(
190
245
 
191
246
  assert len(result) == 1
192
247
  assert result[0].success is True
193
- assert "example_plugin:example_plugin.protocols.my_protocol:Protocol" in LOADED_PLUGINS
248
+
249
+
250
+ @pytest.mark.asyncio
251
+ @pytest.mark.parametrize("install_test_plugin", ["test_module_imports_plugin"], indirect=True)
252
+ async def test_changes_to_plugin_modules_should_be_reflected_after_reload(
253
+ install_test_plugin: Path, load_test_plugins: None, plugin_runner: PluginRunner
254
+ ) -> None:
255
+ """Test that changes to plugin modules are reflected after reloading the plugin."""
256
+ event = EventRequest(type=EventType.UNKNOWN)
257
+
258
+ result = []
259
+ async for response in plugin_runner.HandleEvent(event, None):
260
+ result.append(response)
261
+
262
+ assert len(result) == 1
263
+ assert result[0].success is True
264
+ assert len(result[0].effects) == 1
265
+ assert result[0].effects[0].type == EffectType.LOG
266
+ assert result[0].effects[0].payload == "Successfully imported!"
267
+
268
+ NEW_CODE = """
269
+ def import_me() -> str:
270
+ return "Successfully changed!"
271
+ """
272
+ file_path = install_test_plugin / "other_module" / "base.py"
273
+ file_path.write_text(NEW_CODE, encoding="utf-8")
274
+
275
+ # Reload the plugin
276
+ load_plugins()
277
+
278
+ result = []
279
+ async for response in plugin_runner.HandleEvent(event, None):
280
+ result.append(response)
281
+
282
+ assert len(result) == 1
283
+ assert result[0].success is True
284
+ assert len(result[0].effects) == 1
285
+ assert result[0].effects[0].type == EffectType.LOG
286
+ assert result[0].effects[0].payload == "Successfully changed!"
@@ -1,6 +1,6 @@
1
1
  import pytest
2
2
 
3
- from plugin_runner.sandbox import Sandbox
3
+ from plugin_runner.sandbox import FORBIDDEN_ASSIGNMENTS, Sandbox
4
4
 
5
5
  # Sample code strings for testing various scenarios
6
6
  VALID_CODE = """
@@ -28,11 +28,23 @@ CODE_WITH_FORBIDDEN_FUNC_NAME = """
28
28
  builtins = {}
29
29
  """
30
30
 
31
- SOURCE_CODE_MODULE_OS = """
32
- import os
33
- result = os.listdir('.')
31
+ SOURCE_CODE_MODULE = """
32
+ import module.b
33
+ result = module.b
34
34
  """
35
35
 
36
+ CODE_WITH_FORBIDDEN_ASSIGNMENTS = [
37
+ code
38
+ for var in FORBIDDEN_ASSIGNMENTS
39
+ for code in [
40
+ f"{var} = 'test'",
41
+ f"test = {var} = 'test'",
42
+ f"test = {var} = test2 = 'test'",
43
+ f"(a, (b, c), (d, ({var}, f))) = (1, (2, 3), (4, (5, 6)))",
44
+ f"(a, (b, c), (d, [{var}, f])) = (1, (2, 3), (4, [5, 6]))",
45
+ ]
46
+ ]
47
+
36
48
 
37
49
  def test_valid_code_execution() -> None:
38
50
  """Test execution of valid code in the sandbox."""
@@ -69,6 +81,14 @@ def test_forbidden_name() -> None:
69
81
  sandbox.execute()
70
82
 
71
83
 
84
+ @pytest.mark.parametrize("code", CODE_WITH_FORBIDDEN_ASSIGNMENTS)
85
+ def test_forbidden_assignment(code: str) -> None:
86
+ """Test that forbidden assignments are blocked by Transformer."""
87
+ sandbox = Sandbox(code)
88
+ with pytest.raises(RuntimeError, match="Code is invalid"):
89
+ sandbox.execute()
90
+
91
+
72
92
  def test_code_with_warnings() -> None:
73
93
  """Test that the sandbox captures warnings for restricted names or usage."""
74
94
  code_with_warning = """
@@ -110,16 +130,8 @@ print("Hello, Sandbox!")
110
130
  assert "Hello, Sandbox!" in scope["_print"].txt, "Print output should be captured."
111
131
 
112
132
 
113
- def test_sandbox_module_name_imports_within_package() -> None:
114
- """Test that modules within the same package can be imported."""
115
- sandbox_module_a = Sandbox(source_code=SOURCE_CODE_MODULE_OS, namespace="os.a")
116
- result = sandbox_module_a.execute()
117
-
118
- assert "os" in result
119
-
120
-
121
133
  def test_sandbox_denies_module_name_import_outside_package() -> None:
122
134
  """Test that modules outside the root package cannot be imported."""
123
- sandbox_module_a = Sandbox(source_code=SOURCE_CODE_MODULE_OS, namespace="module.a")
124
- with pytest.raises(ImportError, match="os' is not an allowed import."):
135
+ sandbox_module_a = Sandbox(source_code=SOURCE_CODE_MODULE, namespace="other_module.a")
136
+ with pytest.raises(ImportError, match="module.b' is not an allowed import."):
125
137
  sandbox_module_a.execute()
settings.py CHANGED
@@ -19,7 +19,9 @@ INTEGRATION_TEST_CLIENT_SECRET = os.getenv("INTEGRATION_TEST_CLIENT_SECRET")
19
19
 
20
20
  GRAPHQL_ENDPOINT = os.getenv("GRAPHQL_ENDPOINT", "http://localhost:8000/plugins-graphql")
21
21
 
22
- INSTALLED_APPS = ["canvas_sdk"]
22
+ INSTALLED_APPS = [
23
+ "canvas_sdk.v1",
24
+ ]
23
25
 
24
26
  SECRET_KEY = os.getenv(
25
27
  "SECRET_KEY",
@@ -70,3 +72,13 @@ PLUGIN_DIRECTORY = os.getenv(
70
72
  MANIFEST_FILE_NAME = "CANVAS_MANIFEST.json"
71
73
 
72
74
  SECRETS_FILE_NAME = "SECRETS.json"
75
+
76
+
77
+ TEMPLATES = [
78
+ {
79
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
80
+ "DIRS": [],
81
+ "APP_DIRS": False,
82
+ "OPTIONS": {},
83
+ },
84
+ ]
@@ -1,8 +0,0 @@
1
- # ruff: noqa
2
- # register all models in the app so they can be used with apps.get_model()
3
- from canvas_sdk.v1.data.command import Command
4
- from canvas_sdk.v1.data.condition import Condition
5
- from canvas_sdk.v1.data.note import Note
6
- from canvas_sdk.v1.data.patient import Patient
7
- from canvas_sdk.v1.data.user import CanvasUser
8
- from canvas_sdk.v1.data.questionnaire import Interview