canvas 0.32.0__py3-none-any.whl → 0.33.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.

Files changed (257) hide show
  1. {canvas-0.32.0.dist-info → canvas-0.33.1.dist-info}/METADATA +2 -1
  2. canvas-0.33.1.dist-info/RECORD +272 -0
  3. canvas_generated/messages/effects_pb2.py +2 -2
  4. canvas_generated/messages/effects_pb2.pyi +4 -0
  5. canvas_sdk/__init__.py +3 -0
  6. canvas_sdk/commands/__init__.py +1 -1
  7. canvas_sdk/commands/base.py +3 -0
  8. canvas_sdk/commands/commands/__init__.py +1 -0
  9. canvas_sdk/commands/commands/adjust_prescription.py +3 -0
  10. canvas_sdk/commands/commands/allergy.py +7 -0
  11. canvas_sdk/commands/commands/assess.py +2 -0
  12. canvas_sdk/commands/commands/close_goal.py +3 -0
  13. canvas_sdk/commands/commands/diagnose.py +3 -0
  14. canvas_sdk/commands/commands/exam.py +3 -0
  15. canvas_sdk/commands/commands/family_history.py +3 -0
  16. canvas_sdk/commands/commands/follow_up.py +3 -0
  17. canvas_sdk/commands/commands/goal.py +3 -0
  18. canvas_sdk/commands/commands/history_present_illness.py +3 -0
  19. canvas_sdk/commands/commands/imaging_order.py +3 -0
  20. canvas_sdk/commands/commands/instruct.py +3 -0
  21. canvas_sdk/commands/commands/lab_order.py +3 -0
  22. canvas_sdk/commands/commands/medical_history.py +3 -0
  23. canvas_sdk/commands/commands/medication_statement.py +2 -0
  24. canvas_sdk/commands/commands/past_surgical_history.py +3 -0
  25. canvas_sdk/commands/commands/perform.py +3 -0
  26. canvas_sdk/commands/commands/plan.py +3 -0
  27. canvas_sdk/commands/commands/prescribe.py +8 -0
  28. canvas_sdk/commands/commands/questionnaire/__init__.py +3 -13
  29. canvas_sdk/commands/commands/questionnaire/question.py +10 -0
  30. canvas_sdk/commands/commands/reason_for_visit.py +3 -0
  31. canvas_sdk/commands/commands/refer.py +3 -0
  32. canvas_sdk/commands/commands/refill.py +3 -0
  33. canvas_sdk/commands/commands/remove_allergy.py +3 -0
  34. canvas_sdk/commands/commands/resolve_condition.py +3 -0
  35. canvas_sdk/commands/commands/review_of_systems.py +3 -0
  36. canvas_sdk/commands/commands/stop_medication.py +3 -0
  37. canvas_sdk/commands/commands/structured_assessment.py +3 -0
  38. canvas_sdk/commands/commands/task.py +7 -0
  39. canvas_sdk/commands/commands/update_diagnosis.py +3 -0
  40. canvas_sdk/commands/commands/update_goal.py +3 -0
  41. canvas_sdk/commands/commands/vitals.py +3 -0
  42. canvas_sdk/commands/constants.py +8 -0
  43. canvas_sdk/effects/__init__.py +1 -1
  44. canvas_sdk/effects/banner_alert/__init__.py +1 -1
  45. canvas_sdk/effects/banner_alert/add_banner_alert.py +3 -0
  46. canvas_sdk/effects/banner_alert/remove_banner_alert.py +3 -0
  47. canvas_sdk/effects/base.py +7 -0
  48. canvas_sdk/effects/billing_line_item/__init__.py +5 -1
  49. canvas_sdk/effects/billing_line_item/add_billing_line_item.py +3 -0
  50. canvas_sdk/effects/billing_line_item/remove_billing_line_item.py +3 -0
  51. canvas_sdk/effects/billing_line_item/update_billing_line_item.py +3 -0
  52. canvas_sdk/effects/launch_modal.py +3 -0
  53. canvas_sdk/effects/patient_chart_summary_configuration.py +3 -0
  54. canvas_sdk/effects/patient_portal/__init__.py +1 -0
  55. canvas_sdk/effects/patient_portal/application_configuration.py +3 -0
  56. canvas_sdk/effects/patient_portal/form_result.py +3 -0
  57. canvas_sdk/effects/patient_portal_menu_configuration.py +3 -0
  58. canvas_sdk/effects/patient_profile_configuration.py +6 -1
  59. canvas_sdk/effects/protocol_card/__init__.py +1 -1
  60. canvas_sdk/effects/protocol_card/protocol_card.py +6 -0
  61. canvas_sdk/effects/questionnaire_result.py +3 -0
  62. canvas_sdk/effects/send_invite.py +46 -0
  63. canvas_sdk/effects/show_button.py +3 -0
  64. canvas_sdk/effects/simple_api.py +9 -0
  65. canvas_sdk/effects/surescripts/__init__.py +2 -2
  66. canvas_sdk/effects/surescripts/surescripts_messages.py +7 -0
  67. canvas_sdk/effects/task/__init__.py +6 -1
  68. canvas_sdk/effects/task/task.py +8 -0
  69. canvas_sdk/effects/update_user.py +81 -0
  70. canvas_sdk/effects/widgets/__init__.py +1 -1
  71. canvas_sdk/effects/widgets/portal_widget.py +3 -0
  72. canvas_sdk/events/__init__.py +6 -1
  73. canvas_sdk/events/base.py +3 -0
  74. canvas_sdk/handlers/__init__.py +1 -1
  75. canvas_sdk/handlers/action_button.py +6 -0
  76. canvas_sdk/handlers/application.py +3 -0
  77. canvas_sdk/handlers/base.py +3 -0
  78. canvas_sdk/handlers/cron_task.py +3 -0
  79. canvas_sdk/handlers/simple_api/__init__.py +3 -2
  80. canvas_sdk/handlers/simple_api/api.py +26 -1
  81. canvas_sdk/handlers/simple_api/exceptions.py +10 -0
  82. canvas_sdk/handlers/simple_api/security.py +21 -5
  83. canvas_sdk/handlers/simple_api/tools.py +9 -0
  84. canvas_sdk/protocols/__init__.py +1 -1
  85. canvas_sdk/protocols/base.py +3 -0
  86. canvas_sdk/protocols/clinical_quality_measure.py +6 -1
  87. canvas_sdk/protocols/timeframe.py +3 -0
  88. canvas_sdk/questionnaires/__init__.py +1 -1
  89. canvas_sdk/questionnaires/utils.py +7 -0
  90. canvas_sdk/templates/__init__.py +1 -1
  91. canvas_sdk/templates/utils.py +3 -0
  92. canvas_sdk/utils/__init__.py +1 -1
  93. canvas_sdk/utils/http.py +69 -35
  94. canvas_sdk/utils/plugins.py +4 -0
  95. canvas_sdk/utils/stats.py +11 -0
  96. canvas_sdk/v1/__init__.py +1 -0
  97. canvas_sdk/v1/apps.py +3 -0
  98. canvas_sdk/v1/data/__init__.py +2 -2
  99. canvas_sdk/v1/data/allergy_intolerance.py +3 -0
  100. canvas_sdk/v1/data/appointment.py +7 -0
  101. canvas_sdk/v1/data/assessment.py +3 -0
  102. canvas_sdk/v1/data/banner_alert.py +3 -0
  103. canvas_sdk/v1/data/base.py +3 -0
  104. canvas_sdk/v1/data/billing.py +7 -0
  105. canvas_sdk/v1/data/care_team.py +7 -0
  106. canvas_sdk/v1/data/command.py +3 -0
  107. canvas_sdk/v1/data/common.py +18 -0
  108. canvas_sdk/v1/data/condition.py +7 -0
  109. canvas_sdk/v1/data/coverage.py +14 -0
  110. canvas_sdk/v1/data/detected_issue.py +3 -0
  111. canvas_sdk/v1/data/device.py +3 -0
  112. canvas_sdk/v1/data/imaging.py +7 -0
  113. canvas_sdk/v1/data/lab.py +16 -0
  114. canvas_sdk/v1/data/medication.py +3 -0
  115. canvas_sdk/v1/data/note.py +9 -0
  116. canvas_sdk/v1/data/observation.py +9 -0
  117. canvas_sdk/v1/data/organization.py +3 -0
  118. canvas_sdk/v1/data/patient.py +20 -3
  119. canvas_sdk/v1/data/practicelocation.py +7 -0
  120. canvas_sdk/v1/data/protocol_override.py +7 -0
  121. canvas_sdk/v1/data/questionnaire.py +16 -3
  122. canvas_sdk/v1/data/reason_for_visit.py +3 -0
  123. canvas_sdk/v1/data/staff.py +3 -0
  124. canvas_sdk/v1/data/task.py +12 -0
  125. canvas_sdk/v1/data/team.py +8 -1
  126. canvas_sdk/v1/data/user.py +5 -1
  127. canvas_sdk/v1/models.py +2 -0
  128. canvas_sdk/value_set/__init__.py +1 -0
  129. canvas_sdk/value_set/_utilities.py +16 -0
  130. canvas_sdk/value_set/custom.py +4 -0
  131. canvas_sdk/value_set/hcc2018.py +3 -0
  132. canvas_sdk/value_set/v2022/__init__.py +1 -0
  133. canvas_sdk/value_set/v2022/adverse_event.py +3 -0
  134. canvas_sdk/value_set/v2022/allergy.py +5 -0
  135. canvas_sdk/value_set/v2022/assessment.py +5 -0
  136. canvas_sdk/value_set/v2022/communication.py +5 -0
  137. canvas_sdk/value_set/v2022/condition.py +5 -0
  138. canvas_sdk/value_set/v2022/device.py +5 -0
  139. canvas_sdk/value_set/v2022/diagnostic_study.py +5 -0
  140. canvas_sdk/value_set/v2022/encounter.py +5 -0
  141. canvas_sdk/value_set/v2022/immunization.py +5 -0
  142. canvas_sdk/value_set/v2022/individual_characteristic.py +5 -0
  143. canvas_sdk/value_set/v2022/intervention.py +5 -0
  144. canvas_sdk/value_set/v2022/laboratory_test.py +5 -0
  145. canvas_sdk/value_set/v2022/medication.py +5 -0
  146. canvas_sdk/value_set/v2022/physical_exam.py +5 -0
  147. canvas_sdk/value_set/v2022/procedure.py +5 -0
  148. canvas_sdk/value_set/v2022/symptom.py +3 -0
  149. canvas_sdk/value_set/value_set.py +9 -0
  150. canvas_sdk/views/__init__.py +1 -0
  151. logger/__init__.py +2 -0
  152. logger/logger.py +3 -0
  153. plugin_runner/aws_headers.py +1 -1
  154. plugin_runner/load_all_plugins.py +202 -0
  155. plugin_runner/plugin_runner.py +26 -24
  156. plugin_runner/sandbox.py +497 -115
  157. protobufs/canvas_generated/messages/effects.proto +3 -0
  158. settings.py +5 -2
  159. canvas-0.32.0.dist-info/RECORD +0 -364
  160. canvas_cli/apps/auth/tests.py +0 -155
  161. canvas_cli/apps/plugin/tests.py +0 -85
  162. canvas_cli/conftest.py +0 -28
  163. canvas_cli/tests.py +0 -217
  164. canvas_cli/utils/context/tests.py +0 -131
  165. canvas_cli/utils/print/tests.py +0 -69
  166. canvas_cli/utils/urls/tests.py +0 -12
  167. canvas_cli/utils/validators/tests.py +0 -37
  168. canvas_sdk/commands/tests/protocol/__init__.py +0 -0
  169. canvas_sdk/commands/tests/protocol/tests.py +0 -83
  170. canvas_sdk/commands/tests/schema/__init__.py +0 -0
  171. canvas_sdk/commands/tests/schema/tests.py +0 -108
  172. canvas_sdk/commands/tests/test_base_command.py +0 -81
  173. canvas_sdk/commands/tests/test_utils.py +0 -375
  174. canvas_sdk/commands/tests/unit/__init__.py +0 -0
  175. canvas_sdk/commands/tests/unit/tests.py +0 -278
  176. canvas_sdk/effects/banner_alert/tests.py +0 -288
  177. canvas_sdk/effects/protocol_card/tests.py +0 -191
  178. canvas_sdk/questionnaires/tests/__init__.py +0 -0
  179. canvas_sdk/questionnaires/tests/test_utils.py +0 -74
  180. canvas_sdk/templates/tests/__init__.py +0 -0
  181. canvas_sdk/templates/tests/test_utils.py +0 -43
  182. canvas_sdk/tests/__init__.py +0 -0
  183. canvas_sdk/tests/handlers/__init__.py +0 -0
  184. canvas_sdk/tests/handlers/test_simple_api.py +0 -1167
  185. canvas_sdk/utils/tests.py +0 -72
  186. canvas_sdk/value_set/tests/test_value_sets.py +0 -72
  187. plugin_runner/tests/__init__.py +0 -0
  188. plugin_runner/tests/fixtures/plugins/example_plugin/CANVAS_MANIFEST.json +0 -29
  189. plugin_runner/tests/fixtures/plugins/example_plugin/README.md +0 -12
  190. plugin_runner/tests/fixtures/plugins/example_plugin/__init__.py +0 -0
  191. plugin_runner/tests/fixtures/plugins/example_plugin/protocols/__init__.py +0 -0
  192. plugin_runner/tests/fixtures/plugins/example_plugin/protocols/my_protocol.py +0 -18
  193. plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/CANVAS_MANIFEST.json +0 -38
  194. plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/README.md +0 -11
  195. plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/protocols/__init__.py +0 -0
  196. plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/protocols/my_protocol.py +0 -33
  197. plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/templates/__init__.py +0 -3
  198. plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/templates/base.py +0 -6
  199. plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/utils/__init__.py +0 -5
  200. plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/utils/base.py +0 -4
  201. plugin_runner/tests/fixtures/plugins/test_load_questionnaire/CANVAS_MANIFEST.json +0 -52
  202. plugin_runner/tests/fixtures/plugins/test_load_questionnaire/README.md +0 -11
  203. plugin_runner/tests/fixtures/plugins/test_load_questionnaire/protocols/__init__.py +0 -0
  204. plugin_runner/tests/fixtures/plugins/test_load_questionnaire/protocols/my_protocol.py +0 -39
  205. plugin_runner/tests/fixtures/plugins/test_load_questionnaire/questionnaires/example_questionnaire.yml +0 -61
  206. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/CANVAS_MANIFEST.json +0 -29
  207. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/README.md +0 -12
  208. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/other_module/__init__.py +0 -0
  209. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/other_module/base.py +0 -10
  210. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/protocols/__init__.py +0 -0
  211. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/protocols/my_protocol.py +0 -18
  212. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/CANVAS_MANIFEST.json +0 -29
  213. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/README.md +0 -12
  214. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/other_module/__init__.py +0 -0
  215. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/other_module/base.py +0 -10
  216. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/protocols/__init__.py +0 -0
  217. plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/protocols/my_protocol.py +0 -18
  218. plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v1/CANVAS_MANIFEST.json +0 -29
  219. plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v1/README.md +0 -12
  220. plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v1/other_module/__init__.py +0 -0
  221. plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v1/other_module/base.py +0 -3
  222. plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v1/protocols/__init__.py +0 -0
  223. plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v1/protocols/my_protocol.py +0 -18
  224. plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v2/CANVAS_MANIFEST.json +0 -29
  225. plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v2/README.md +0 -12
  226. plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v2/other_module/__init__.py +0 -0
  227. plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v2/other_module/base.py +0 -6
  228. plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v2/protocols/__init__.py +0 -0
  229. plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v2/protocols/my_protocol.py +0 -18
  230. plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v3/CANVAS_MANIFEST.json +0 -29
  231. plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v3/README.md +0 -12
  232. plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v3/other_module/__init__.py +0 -0
  233. plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v3/other_module/base.py +0 -8
  234. plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v3/protocols/__init__.py +0 -0
  235. plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v3/protocols/my_protocol.py +0 -18
  236. plugin_runner/tests/fixtures/plugins/test_module_imports_plugin/CANVAS_MANIFEST.json +0 -29
  237. plugin_runner/tests/fixtures/plugins/test_module_imports_plugin/README.md +0 -12
  238. plugin_runner/tests/fixtures/plugins/test_module_imports_plugin/other_module/__init__.py +0 -0
  239. plugin_runner/tests/fixtures/plugins/test_module_imports_plugin/other_module/base.py +0 -3
  240. plugin_runner/tests/fixtures/plugins/test_module_imports_plugin/protocols/__init__.py +0 -0
  241. plugin_runner/tests/fixtures/plugins/test_module_imports_plugin/protocols/my_protocol.py +0 -18
  242. plugin_runner/tests/fixtures/plugins/test_render_template/CANVAS_MANIFEST.json +0 -47
  243. plugin_runner/tests/fixtures/plugins/test_render_template/README.md +0 -11
  244. plugin_runner/tests/fixtures/plugins/test_render_template/protocols/__init__.py +0 -0
  245. plugin_runner/tests/fixtures/plugins/test_render_template/protocols/my_protocol.py +0 -43
  246. plugin_runner/tests/fixtures/plugins/test_render_template/templates/template.html +0 -10
  247. plugin_runner/tests/fixtures/plugins/test_simple_api/CANVAS_MANIFEST.json +0 -47
  248. plugin_runner/tests/fixtures/plugins/test_simple_api/README.md +0 -11
  249. plugin_runner/tests/fixtures/plugins/test_simple_api/__init__.py +0 -0
  250. plugin_runner/tests/fixtures/plugins/test_simple_api/protocols/__init__.py +0 -0
  251. plugin_runner/tests/fixtures/plugins/test_simple_api/protocols/my_protocol.py +0 -43
  252. plugin_runner/tests/test_application.py +0 -65
  253. plugin_runner/tests/test_plugin_installer.py +0 -127
  254. plugin_runner/tests/test_plugin_runner.py +0 -388
  255. plugin_runner/tests/test_sandbox.py +0 -137
  256. {canvas-0.32.0.dist-info → canvas-0.33.1.dist-info}/WHEEL +0 -0
  257. {canvas-0.32.0.dist-info → canvas-0.33.1.dist-info}/entry_points.txt +0 -0
@@ -1,127 +0,0 @@
1
- import json
2
- import tarfile
3
- import tempfile
4
- from pathlib import Path
5
- from unittest.mock import MagicMock, patch
6
-
7
- from pytest_mock import MockerFixture
8
-
9
- from plugin_runner.installation import (
10
- PluginAttributes,
11
- _extract_rows_to_dict,
12
- download_plugin,
13
- install_plugins,
14
- uninstall_plugin,
15
- )
16
-
17
-
18
- def _create_tarball(name: str) -> Path:
19
- # Create a temporary tarball file
20
- temp_dir = tempfile.mkdtemp()
21
- tarball_path = Path(temp_dir) / f"{name}.tar.gz"
22
-
23
- # Add some files to the tarball
24
- with tarfile.open(tarball_path, "w:gz") as tar:
25
- for i in range(3):
26
- file_path = Path(temp_dir) / f"file{i}.txt"
27
- file_path.write_text(f"Content of file {i}")
28
- tar.add(file_path, arcname=f"file{i}.txt")
29
-
30
- # Return a Path handle to the tarball
31
- return tarball_path
32
-
33
-
34
- def test_extract_rows_to_dict() -> None:
35
- """Test that database rows can be extracted to a dictionary with secrets appropriately attributed to plugin."""
36
- rows = [
37
- {
38
- "name": "plugin1",
39
- "version": "1.0",
40
- "package": "package1",
41
- "key": "key1",
42
- "value": "value1",
43
- },
44
- {
45
- "name": "plugin1",
46
- "version": "1.0",
47
- "package": "package1",
48
- "key": "key2",
49
- "value": "value2",
50
- },
51
- {"name": "plugin2", "version": "2.0", "package": "package2", "key": None, "value": None},
52
- ]
53
-
54
- expected_output = {
55
- "plugin1": {
56
- "version": "1.0",
57
- "package": "package1",
58
- "secrets": {"key1": "value1", "key2": "value2"},
59
- },
60
- "plugin2": {
61
- "version": "2.0",
62
- "package": "package2",
63
- "secrets": {},
64
- },
65
- }
66
-
67
- result = _extract_rows_to_dict(rows)
68
- assert result == expected_output
69
-
70
-
71
- def test_plugin_installation_from_tarball(mocker: MockerFixture) -> None:
72
- """Test that plugins can be installed from tarballs."""
73
- mock_plugins = {
74
- "plugin1": PluginAttributes(
75
- version="1.0", package="plugins/plugin1.tar.gz", secrets={"key1": "value1"}
76
- ),
77
- "plugin2": PluginAttributes(
78
- version="1.0", package="plugins/plugin2.tar", secrets={"key2": "value2"}
79
- ),
80
- }
81
-
82
- tarball_1 = _create_tarball("plugin1")
83
- tarball_2 = _create_tarball("plugin2")
84
-
85
- mocker.patch("plugin_runner.installation.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
-
96
- mocker.patch(
97
- "plugin_runner.installation.download_plugin",
98
- side_effect=mock_download_plugin,
99
- )
100
-
101
- install_plugins()
102
- assert Path("plugin_runner/tests/data/plugins/plugin1").exists()
103
- assert Path("plugin_runner/tests/data/plugins/plugin1/SECRETS.json").exists()
104
- with open("plugin_runner/tests/data/plugins/plugin1/SECRETS.json") as f:
105
- assert json.load(f) == mock_plugins["plugin1"]["secrets"]
106
- assert Path("plugin_runner/tests/data/plugins/plugin2").exists()
107
- assert Path("plugin_runner/tests/data/plugins/plugin2/SECRETS.json").exists()
108
- with open("plugin_runner/tests/data/plugins/plugin2/SECRETS.json") as f:
109
- assert json.load(f) == mock_plugins["plugin2"]["secrets"]
110
-
111
- uninstall_plugin("plugin1")
112
- uninstall_plugin("plugin2")
113
- assert not Path("plugin_runner/tests/data/plugins/plugin1").exists()
114
- assert not Path("plugin_runner/tests/data/plugins/plugin2").exists()
115
-
116
-
117
- def test_download(mocker: MockerFixture) -> None:
118
- """Test that the plugin package can be written to disk, mocking out S3."""
119
- mock_response = MagicMock()
120
- mock_response.status_code = 200
121
- mock_response.content = b"some content in a file"
122
- with patch("requests.request", return_value=mock_response) as mock_request:
123
- plugin_package = "plugins/plugin1.tar.gz"
124
- with download_plugin(plugin_package) as plugin_path:
125
- assert plugin_path.exists()
126
- assert plugin_path.read_bytes() == b"some content in a file"
127
- mock_request.assert_called_once()
@@ -1,388 +0,0 @@
1
- import asyncio
2
- import json
3
- import logging
4
- import pickle
5
- import shutil
6
- from base64 import b64encode
7
- from http import HTTPStatus
8
- from pathlib import Path
9
- from typing import Any
10
- from unittest.mock import AsyncMock, MagicMock, patch
11
-
12
- import pytest
13
-
14
- from canvas_generated.messages.effects_pb2 import EffectType
15
- from canvas_generated.messages.plugins_pb2 import ReloadPluginsRequest
16
- from canvas_sdk.effects.simple_api import Response
17
- from canvas_sdk.events import Event, EventRequest, EventType
18
- from plugin_runner.plugin_runner import (
19
- EVENT_HANDLER_MAP,
20
- LOADED_PLUGINS,
21
- PluginRunner,
22
- load_or_reload_plugin,
23
- load_plugins,
24
- synchronize_plugins,
25
- )
26
-
27
-
28
- @pytest.fixture
29
- def plugin_runner() -> PluginRunner:
30
- """Fixture to initialize PluginRunner with mocks."""
31
- runner = PluginRunner()
32
- runner.statsd_client = MagicMock() # type: ignore[attr-defined]
33
- return runner
34
-
35
-
36
- @pytest.mark.parametrize("install_test_plugin", ["example_plugin"], indirect=True)
37
- def test_load_plugins_with_valid_plugin(install_test_plugin: Path, load_test_plugins: None) -> None:
38
- """Test loading plugins with a valid plugin."""
39
- assert "example_plugin:example_plugin.protocols.my_protocol:Protocol" in LOADED_PLUGINS
40
- assert (
41
- LOADED_PLUGINS["example_plugin:example_plugin.protocols.my_protocol:Protocol"]["active"]
42
- is True
43
- )
44
-
45
-
46
- @pytest.mark.asyncio
47
- @pytest.mark.parametrize("install_test_plugin", ["test_module_imports_plugin"], indirect=True)
48
- async def test_load_plugins_with_plugin_that_imports_other_modules_within_plugin_package(
49
- install_test_plugin: Path, plugin_runner: PluginRunner, load_test_plugins: None
50
- ) -> None:
51
- """Test loading plugins with a valid plugin that imports other modules within the current plugin package."""
52
- assert (
53
- "test_module_imports_plugin:test_module_imports_plugin.protocols.my_protocol:Protocol"
54
- in LOADED_PLUGINS
55
- )
56
- assert (
57
- LOADED_PLUGINS[
58
- "test_module_imports_plugin:test_module_imports_plugin.protocols.my_protocol:Protocol"
59
- ]["active"]
60
- is True
61
- )
62
-
63
- result = [
64
- response
65
- async for response in plugin_runner.HandleEvent(EventRequest(type=EventType.UNKNOWN), None)
66
- ]
67
-
68
- assert len(result) == 1
69
- assert result[0].success is True
70
- assert len(result[0].effects) == 1
71
- assert result[0].effects[0].type == EffectType.LOG
72
- assert result[0].effects[0].payload == "Successfully imported!"
73
-
74
-
75
- @pytest.mark.parametrize(
76
- "install_test_plugin",
77
- [
78
- "test_module_imports_outside_plugin_v1",
79
- "test_module_imports_outside_plugin_v2",
80
- "test_module_imports_outside_plugin_v3",
81
- ],
82
- indirect=True,
83
- )
84
- def test_load_plugins_with_plugin_that_imports_other_modules_outside_plugin_package(
85
- install_test_plugin: Path, caplog: pytest.LogCaptureFixture
86
- ) -> None:
87
- """Test loading plugins with an invalid plugin that imports other modules outside the current plugin package."""
88
- with caplog.at_level(logging.ERROR):
89
- load_or_reload_plugin(install_test_plugin)
90
-
91
- assert any("Error importing module" in record.message for record in caplog.records), (
92
- "log.error() was not called with the expected message."
93
- )
94
-
95
-
96
- @pytest.mark.parametrize(
97
- "install_test_plugin",
98
- [
99
- "test_module_forbidden_imports_plugin",
100
- ],
101
- indirect=True,
102
- )
103
- def test_load_plugins_with_plugin_that_imports_forbidden_modules(
104
- install_test_plugin: Path, caplog: pytest.LogCaptureFixture
105
- ) -> None:
106
- """Test loading plugins with an invalid plugin that imports forbidden modules."""
107
- with caplog.at_level(logging.ERROR):
108
- load_or_reload_plugin(install_test_plugin)
109
-
110
- assert any("Error importing module" in record.message for record in caplog.records), (
111
- "log.error() was not called with the expected message."
112
- )
113
-
114
-
115
- @pytest.mark.parametrize(
116
- "install_test_plugin",
117
- [
118
- "test_module_forbidden_imports_runtime_plugin",
119
- ],
120
- indirect=True,
121
- )
122
- def test_load_plugins_with_plugin_that_imports_forbidden_modules_at_runtime(
123
- install_test_plugin: Path,
124
- ) -> None:
125
- """Test loading plugins with an invalid plugin that imports forbidden modules at runtime."""
126
- with pytest.raises(ImportError, match="is not an allowed import."):
127
- load_or_reload_plugin(install_test_plugin)
128
- class_handler = LOADED_PLUGINS[
129
- "test_module_forbidden_imports_runtime_plugin:test_module_forbidden_imports_runtime_plugin.protocols.my_protocol:Protocol"
130
- ]["class"]
131
- class_handler(Event(EventRequest(type=EventType.UNKNOWN))).compute()
132
-
133
-
134
- @pytest.mark.parametrize(
135
- "install_test_plugin",
136
- [
137
- "test_implicit_imports_plugin",
138
- ],
139
- indirect=True,
140
- )
141
- def test_plugin_that_implicitly_imports_allowed_modules(
142
- install_test_plugin: Path, caplog: pytest.LogCaptureFixture
143
- ) -> None:
144
- """Test loading plugins with a plugin that implicitly imports allowed modules."""
145
- with caplog.at_level(logging.INFO):
146
- load_or_reload_plugin(install_test_plugin)
147
- class_handler = LOADED_PLUGINS[
148
- "test_implicit_imports_plugin:test_implicit_imports_plugin.protocols.my_protocol:Allowed"
149
- ]["class"]
150
- class_handler(Event(EventRequest(type=EventType.UNKNOWN))).compute()
151
-
152
- assert any("Hello, World!" in record.message for record in caplog.records), (
153
- "log.info() with Template.render() was not called."
154
- )
155
-
156
-
157
- @pytest.mark.parametrize(
158
- "install_test_plugin",
159
- [
160
- "test_implicit_imports_plugin",
161
- ],
162
- indirect=True,
163
- )
164
- def test_plugin_that_implicitly_imports_forbidden_modules(
165
- install_test_plugin: Path, caplog: pytest.LogCaptureFixture
166
- ) -> None:
167
- """Test loading plugins with an invalid plugin that implicitly imports forbidden modules."""
168
- with (
169
- caplog.at_level(logging.INFO),
170
- pytest.raises(ImportError, match="'os' is not an allowed import."),
171
- ):
172
- load_or_reload_plugin(install_test_plugin)
173
- class_handler = LOADED_PLUGINS[
174
- "test_implicit_imports_plugin:test_implicit_imports_plugin.protocols.my_protocol:Forbidden"
175
- ]["class"]
176
- class_handler(Event(EventRequest(type=EventType.UNKNOWN))).compute()
177
-
178
- assert any("os list dir" in record.message for record in caplog.records) is False, (
179
- "log.info() with os.listdir() was called."
180
- )
181
-
182
-
183
- @pytest.mark.parametrize("install_test_plugin", ["example_plugin"], indirect=True)
184
- def test_reload_plugin(install_test_plugin: Path, load_test_plugins: None) -> None:
185
- """Test reloading a plugin."""
186
- load_plugins()
187
-
188
- assert "example_plugin:example_plugin.protocols.my_protocol:Protocol" in LOADED_PLUGINS
189
- assert (
190
- LOADED_PLUGINS["example_plugin:example_plugin.protocols.my_protocol:Protocol"]["active"]
191
- is True
192
- )
193
-
194
-
195
- @pytest.mark.parametrize("install_test_plugin", ["example_plugin"], indirect=True)
196
- def test_remove_plugin_should_be_removed_from_loaded_plugins(
197
- install_test_plugin: Path, load_test_plugins: None
198
- ) -> None:
199
- """Test removing a plugin."""
200
- assert "example_plugin:example_plugin.protocols.my_protocol:Protocol" in LOADED_PLUGINS
201
- shutil.rmtree(install_test_plugin)
202
- load_plugins()
203
- assert "example_plugin:example_plugin.protocols.my_protocol:Protocol" not in LOADED_PLUGINS
204
-
205
-
206
- @pytest.mark.parametrize("install_test_plugin", ["example_plugin"], indirect=True)
207
- @pytest.mark.parametrize("load_test_plugins", [None], indirect=True)
208
- def test_load_plugins_should_refresh_event_protocol_map(
209
- load_test_plugins: None, install_test_plugin: Path
210
- ) -> None:
211
- """Test that the event protocol map is refreshed when loading plugins."""
212
- assert EVENT_HANDLER_MAP == {}
213
- load_plugins()
214
- assert EventType.Name(EventType.UNKNOWN) in EVENT_HANDLER_MAP
215
- assert EVENT_HANDLER_MAP[EventType.Name(EventType.UNKNOWN)] == [
216
- "example_plugin:example_plugin.protocols.my_protocol:Protocol"
217
- ]
218
-
219
-
220
- @pytest.mark.asyncio
221
- @pytest.mark.parametrize("install_test_plugin", ["example_plugin"], indirect=True)
222
- async def test_handle_plugin_event_returns_expected_result(
223
- install_test_plugin: Path, plugin_runner: PluginRunner, load_test_plugins: None
224
- ) -> None:
225
- """Test that HandleEvent successfully calls the relevant plugins and returns the expected result."""
226
- event = EventRequest(type=EventType.UNKNOWN)
227
-
228
- result = []
229
- async for response in plugin_runner.HandleEvent(event, None):
230
- result.append(response)
231
-
232
- assert len(result) == 1
233
- assert result[0].success is True
234
- assert len(result[0].effects) == 1
235
- assert result[0].effects[0].type == EffectType.LOG
236
- assert result[0].effects[0].payload == "Hello, world!"
237
-
238
-
239
- @pytest.mark.asyncio
240
- async def test_reload_plugins_event_handler_successfully_publishes_message(
241
- plugin_runner: PluginRunner,
242
- ) -> None:
243
- """Test ReloadPlugins Event handler successfully publishes a message with restart action."""
244
- with patch(
245
- "plugin_runner.plugin_runner.publish_message", new_callable=AsyncMock
246
- ) as mock_publish_message:
247
- request = ReloadPluginsRequest()
248
-
249
- result = []
250
- async for response in plugin_runner.ReloadPlugins(request, None):
251
- result.append(response)
252
-
253
- mock_publish_message.assert_called_once_with(message={"action": "reload"})
254
-
255
- assert len(result) == 1
256
- assert result[0].success is True
257
-
258
-
259
- @pytest.mark.asyncio
260
- async def test_synchronize_plugins_calls_install_and_load_plugins() -> None:
261
- """Test that synchronize_plugins calls install_plugins and load_plugins."""
262
- with (
263
- patch("plugin_runner.plugin_runner.get_client", new_callable=MagicMock) as mock_get_client,
264
- patch(
265
- "plugin_runner.plugin_runner.install_plugins", new_callable=AsyncMock
266
- ) as mock_install_plugins,
267
- patch(
268
- "plugin_runner.plugin_runner.load_plugins", new_callable=AsyncMock
269
- ) as mock_load_plugins,
270
- ):
271
- mock_client = AsyncMock()
272
- mock_pubsub = AsyncMock()
273
- mock_get_client.return_value = (mock_client, mock_pubsub)
274
- mock_pubsub.get_message.return_value = {
275
- "type": "pmessage",
276
- "data": pickle.dumps({"action": "reload"}),
277
- }
278
-
279
- task = asyncio.create_task(synchronize_plugins(run_once=True))
280
- await asyncio.sleep(0.1) # Give some time for the coroutine to run
281
- task.cancel()
282
-
283
- mock_install_plugins.assert_called_once()
284
- mock_load_plugins.assert_called_once()
285
-
286
-
287
- @pytest.mark.asyncio
288
- @pytest.mark.parametrize("install_test_plugin", ["test_module_imports_plugin"], indirect=True)
289
- async def test_changes_to_plugin_modules_should_be_reflected_after_reload(
290
- install_test_plugin: Path, load_test_plugins: None, plugin_runner: PluginRunner
291
- ) -> None:
292
- """Test that changes to plugin modules are reflected after reloading the plugin."""
293
- event = EventRequest(type=EventType.UNKNOWN)
294
-
295
- result = []
296
- async for response in plugin_runner.HandleEvent(event, None):
297
- result.append(response)
298
-
299
- assert len(result) == 1
300
- assert result[0].success is True
301
- assert len(result[0].effects) == 1
302
- assert result[0].effects[0].type == EffectType.LOG
303
- assert result[0].effects[0].payload == "Successfully imported!"
304
-
305
- NEW_CODE = """
306
- def import_me() -> str:
307
- return "Successfully changed!"
308
- """
309
- file_path = install_test_plugin / "other_module" / "base.py"
310
- file_path.write_text(NEW_CODE, encoding="utf-8")
311
-
312
- # Reload the plugin
313
- load_plugins()
314
-
315
- result = []
316
- async for response in plugin_runner.HandleEvent(event, None):
317
- result.append(response)
318
-
319
- assert len(result) == 1
320
- assert result[0].success is True
321
- assert len(result[0].effects) == 1
322
- assert result[0].effects[0].type == EffectType.LOG
323
- assert result[0].effects[0].payload == "Successfully changed!"
324
-
325
-
326
- @pytest.mark.asyncio
327
- @pytest.mark.parametrize(
328
- argnames="context,status_code",
329
- argvalues=[
330
- (
331
- {
332
- "plugin_name": "test_simple_api",
333
- "method": "GET",
334
- "path": "/route",
335
- "query_string": "",
336
- "body": b64encode(b"").decode(),
337
- "headers": {},
338
- },
339
- HTTPStatus.OK,
340
- ),
341
- (
342
- {
343
- "plugin_name": "test_simple_api",
344
- "method": "GET",
345
- "path": "/notfound",
346
- "query_string": "",
347
- "body": b64encode(b"").decode(),
348
- "headers": {},
349
- },
350
- HTTPStatus.NOT_FOUND,
351
- ),
352
- (
353
- {
354
- "plugin_name": "test_simple_api",
355
- "method": "GET",
356
- "path": "/error",
357
- "query_string": "",
358
- "body": b64encode(b"").decode(),
359
- "headers": {},
360
- },
361
- HTTPStatus.INTERNAL_SERVER_ERROR,
362
- ),
363
- ],
364
- ids=["success", "not found error", "multiple handlers error"],
365
- )
366
- @pytest.mark.parametrize("install_test_plugin", ["test_simple_api"], indirect=True)
367
- async def test_simple_api(
368
- install_test_plugin: Path,
369
- load_test_plugins: None,
370
- plugin_runner: PluginRunner,
371
- context: dict[str, Any],
372
- status_code: HTTPStatus,
373
- ) -> None:
374
- """Test that the PluginRunner returns responses to SimpleAPI request events."""
375
- event = EventRequest(
376
- type=EventType.SIMPLE_API_REQUEST,
377
- context=json.dumps(context),
378
- )
379
-
380
- result = []
381
- async for response in plugin_runner.HandleEvent(event, None):
382
- result.append(response)
383
-
384
- expected_response = Response(status_code=status_code).apply()
385
- if status_code == HTTPStatus.OK:
386
- expected_response.plugin_name = "test_simple_api"
387
-
388
- assert result[0].effects == [expected_response]
@@ -1,137 +0,0 @@
1
- import pytest
2
-
3
- from plugin_runner.sandbox import FORBIDDEN_ASSIGNMENTS, Sandbox
4
-
5
- # Sample code strings for testing various scenarios
6
- VALID_CODE = """
7
- x = 10
8
- y = 20
9
- result = x + y
10
- """
11
-
12
- CODE_WITH_RESTRICTED_IMPORT = """
13
- import os
14
- result = os.listdir('.')
15
- """
16
-
17
- CODE_WITH_PLUGIN_RUNNER_SETTING_IMPORT = """
18
- import settings
19
- result = settings.AWS_SECRET_ACCESS_KEY
20
- """
21
-
22
- CODE_WITH_ALLOWED_IMPORT = """
23
- import json
24
- result = json.dumps({"key": "value"})
25
- """
26
-
27
- CODE_WITH_FORBIDDEN_FUNC_NAME = """
28
- builtins = {}
29
- """
30
-
31
- SOURCE_CODE_MODULE = """
32
- import module.b
33
- result = module.b
34
- """
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
-
48
-
49
- def test_valid_code_execution() -> None:
50
- """Test execution of valid code in the sandbox."""
51
- sandbox = Sandbox(VALID_CODE)
52
- scope = sandbox.execute()
53
- assert scope["result"] == 30, "The code should compute result as 30."
54
-
55
-
56
- def test_disallowed_import() -> None:
57
- """Test that restricted imports are not allowed."""
58
- sandbox = Sandbox(CODE_WITH_RESTRICTED_IMPORT)
59
- with pytest.raises(ImportError, match="'os' is not an allowed import."):
60
- sandbox.execute()
61
-
62
-
63
- def test_plugin_runner_settings_import() -> None:
64
- """Test that imports of plugin runner settings are not allowed."""
65
- sandbox = Sandbox(CODE_WITH_PLUGIN_RUNNER_SETTING_IMPORT)
66
- with pytest.raises(ImportError, match="'settings' is not an allowed import."):
67
- sandbox.execute()
68
-
69
-
70
- def test_allowed_import() -> None:
71
- """Test that allowed imports (from ALLOWED_MODULES) work correctly."""
72
- sandbox = Sandbox(CODE_WITH_ALLOWED_IMPORT)
73
- scope = sandbox.execute()
74
- assert scope["result"] == '{"key": "value"}', "JSON encoding should work with allowed imports."
75
-
76
-
77
- def test_forbidden_name() -> None:
78
- """Test that forbidden function names are blocked by Transformer."""
79
- sandbox = Sandbox(CODE_WITH_FORBIDDEN_FUNC_NAME)
80
- with pytest.raises(RuntimeError, match="Code is invalid"):
81
- sandbox.execute()
82
-
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
-
92
- def test_code_with_warnings() -> None:
93
- """Test that the sandbox captures warnings for restricted names or usage."""
94
- code_with_warning = """
95
- _x = 5
96
- result = _x
97
- """
98
- sandbox = Sandbox(code_with_warning)
99
- assert sandbox.warnings, "There should be warnings for using restricted names."
100
- scope = sandbox.execute()
101
- assert scope["result"] == 5, "Code should execute despite warnings."
102
-
103
-
104
- def test_compile_errors() -> None:
105
- """Test that compile errors are detected for invalid syntax."""
106
- invalid_code = """
107
- def missing_colon()
108
- return 42
109
- """
110
- sandbox = Sandbox(invalid_code)
111
- with pytest.raises(RuntimeError, match="Code is invalid"):
112
- sandbox.execute()
113
-
114
-
115
- def test_sandbox_scope() -> None:
116
- """Verify the sandbox scope includes expected built-ins and utility functions."""
117
- sandbox = Sandbox(VALID_CODE)
118
- scope = sandbox.execute()
119
- assert "any" in scope["__builtins__"]
120
- assert scope["__builtins__"]["any"] == any, "'any' function should be accessible in sandbox."
121
-
122
-
123
- def test_print_collector() -> None:
124
- """Ensure that PrintCollector is used for capturing prints."""
125
- code_with_print = """
126
- print("Hello, Sandbox!")
127
- """
128
- sandbox = Sandbox(code_with_print)
129
- scope = sandbox.execute()
130
- assert "Hello, Sandbox!" in scope["_print"].txt, "Print output should be captured."
131
-
132
-
133
- def test_sandbox_denies_module_name_import_outside_package() -> None:
134
- """Test that modules outside the root package cannot be imported."""
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."):
137
- sandbox_module_a.execute()