canvas 0.15.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 (46) hide show
  1. {canvas-0.15.0.dist-info → canvas-0.16.0.dist-info}/METADATA +2 -2
  2. {canvas-0.15.0.dist-info → canvas-0.16.0.dist-info}/RECORD +46 -32
  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/utils/validators/manifest_schema.py +9 -2
  6. canvas_generated/messages/effects_pb2.py +2 -2
  7. canvas_generated/messages/effects_pb2.pyi +14 -0
  8. canvas_generated/messages/events_pb2.py +2 -2
  9. canvas_generated/messages/events_pb2.pyi +40 -0
  10. canvas_sdk/effects/launch_modal.py +14 -3
  11. canvas_sdk/handlers/action_button.py +33 -16
  12. canvas_sdk/templates/__init__.py +3 -0
  13. canvas_sdk/templates/tests/__init__.py +0 -0
  14. canvas_sdk/templates/tests/test_utils.py +43 -0
  15. canvas_sdk/templates/utils.py +44 -0
  16. canvas_sdk/v1/data/__init__.py +23 -1
  17. canvas_sdk/v1/data/allergy_intolerance.py +22 -2
  18. canvas_sdk/v1/data/appointment.py +56 -0
  19. canvas_sdk/v1/data/assessment.py +40 -0
  20. canvas_sdk/v1/data/base.py +35 -22
  21. canvas_sdk/v1/data/billing.py +2 -2
  22. canvas_sdk/v1/data/care_team.py +60 -0
  23. canvas_sdk/v1/data/command.py +1 -1
  24. canvas_sdk/v1/data/common.py +53 -0
  25. canvas_sdk/v1/data/condition.py +19 -3
  26. canvas_sdk/v1/data/coverage.py +294 -0
  27. canvas_sdk/v1/data/detected_issue.py +1 -0
  28. canvas_sdk/v1/data/lab.py +26 -3
  29. canvas_sdk/v1/data/medication.py +13 -3
  30. canvas_sdk/v1/data/note.py +5 -1
  31. canvas_sdk/v1/data/observation.py +15 -3
  32. canvas_sdk/v1/data/patient.py +140 -1
  33. canvas_sdk/v1/data/protocol_override.py +18 -2
  34. canvas_sdk/v1/data/questionnaire.py +15 -2
  35. canvas_sdk/value_set/hcc2018.py +55369 -0
  36. plugin_runner/sandbox.py +28 -0
  37. plugin_runner/tests/fixtures/plugins/test_render_template/CANVAS_MANIFEST.json +47 -0
  38. plugin_runner/tests/fixtures/plugins/test_render_template/README.md +11 -0
  39. plugin_runner/tests/fixtures/plugins/test_render_template/protocols/__init__.py +0 -0
  40. plugin_runner/tests/fixtures/plugins/test_render_template/protocols/my_protocol.py +43 -0
  41. plugin_runner/tests/fixtures/plugins/test_render_template/templates/template.html +10 -0
  42. plugin_runner/tests/test_plugin_runner.py +0 -46
  43. plugin_runner/tests/test_sandbox.py +21 -1
  44. settings.py +10 -0
  45. {canvas-0.15.0.dist-info → canvas-0.16.0.dist-info}/WHEEL +0 -0
  46. {canvas-0.15.0.dist-info → canvas-0.16.0.dist-info}/entry_points.txt +0 -0
plugin_runner/sandbox.py CHANGED
@@ -46,6 +46,7 @@ ALLOWED_MODULES = frozenset(
46
46
  "canvas_sdk.handlers",
47
47
  "canvas_sdk.protocols",
48
48
  "canvas_sdk.utils",
49
+ "canvas_sdk.templates",
49
50
  "canvas_sdk.v1",
50
51
  "canvas_sdk.value_set",
51
52
  "canvas_sdk.views",
@@ -81,6 +82,14 @@ ALLOWED_MODULES = frozenset(
81
82
  )
82
83
 
83
84
 
85
+ ##
86
+ # FORBIDDEN_ASSIGNMENTS
87
+ #
88
+ # The names in this list are forbidden to be assigned to in a sandboxed runtime.
89
+ #
90
+ FORBIDDEN_ASSIGNMENTS = frozenset(["__name__", "__is_plugin__"])
91
+
92
+
84
93
  def _is_known_module(name: str) -> bool:
85
94
  return any(name.startswith(m) for m in ALLOWED_MODULES)
86
95
 
@@ -174,6 +183,24 @@ class Sandbox:
174
183
  elif name in FORBIDDEN_FUNC_NAMES:
175
184
  self.error(node, f'"{name}" is a reserved name.')
176
185
 
186
+ def visit_Assign(self, node: ast.Assign) -> ast.AST:
187
+ """Check for forbidden assignments."""
188
+ for target in node.targets:
189
+ if isinstance(target, ast.Name) and target.id in FORBIDDEN_ASSIGNMENTS:
190
+ self.error(node, f"Assignments to '{target.id}' are not allowed.")
191
+ elif isinstance(target, ast.Tuple | ast.List):
192
+ self.check_for_name_in_iterable(target)
193
+
194
+ return super().visit_Assign(node)
195
+
196
+ def check_for_name_in_iterable(self, iterable_node: ast.Tuple | ast.List) -> None:
197
+ """Check if any element of an iterable is a forbidden assignment."""
198
+ for elt in iterable_node.elts:
199
+ if isinstance(elt, ast.Name) and elt.id in FORBIDDEN_ASSIGNMENTS:
200
+ self.error(iterable_node, f"Assignments to '{elt.id}' are not allowed.")
201
+ elif isinstance(elt, ast.Tuple | ast.List):
202
+ self.check_for_name_in_iterable(elt)
203
+
177
204
  def visit_Attribute(self, node: ast.Attribute) -> ast.AST:
178
205
  """Checks and mutates attribute access/assignment.
179
206
 
@@ -272,6 +299,7 @@ class Sandbox:
272
299
  },
273
300
  "__metaclass__": type,
274
301
  "__name__": self.namespace,
302
+ "__is_plugin__": True,
275
303
  "_write_": _unrestricted,
276
304
  "_getiter_": _unrestricted,
277
305
  "_getitem_": default_guarded_getitem,
@@ -0,0 +1,47 @@
1
+ {
2
+ "sdk_version": "0.1.4",
3
+ "plugin_version": "0.0.1",
4
+ "name": "test_render_template",
5
+ "description": "Edit the description in CANVAS_MANIFEST.json",
6
+ "components": {
7
+ "protocols": [
8
+ {
9
+ "class": "test_render_template.protocols.my_protocol:ValidTemplate",
10
+ "description": "A protocol that does xyz...",
11
+ "data_access": {
12
+ "event": "",
13
+ "read": [],
14
+ "write": []
15
+ }
16
+ },
17
+ {
18
+ "class": "test_render_template.protocols.my_protocol:InvalidTemplate",
19
+ "description": "A protocol that does xyz...",
20
+ "data_access": {
21
+ "event": "",
22
+ "read": [],
23
+ "write": []
24
+ }
25
+ },
26
+ {
27
+ "class": "test_render_template.protocols.my_protocol:ForbiddenTemplate",
28
+ "description": "A protocol that does xyz...",
29
+ "data_access": {
30
+ "event": "",
31
+ "read": [],
32
+ "write": []
33
+ }
34
+ }
35
+ ],
36
+ "commands": [],
37
+ "content": [],
38
+ "effects": [],
39
+ "views": []
40
+ },
41
+ "secrets": [],
42
+ "tags": {},
43
+ "references": [],
44
+ "license": "",
45
+ "diagram": false,
46
+ "readme": "./README.md"
47
+ }
@@ -0,0 +1,11 @@
1
+ test_render_template
2
+ ====================
3
+
4
+ ## Description
5
+
6
+ A description of this plugin
7
+
8
+ ### Important Note!
9
+
10
+ The CANVAS_MANIFEST.json is used when installing your plugin. Please ensure it
11
+ gets updated if you add, remove, or rename protocols.
@@ -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>
@@ -1,6 +1,5 @@
1
1
  import logging
2
2
  import shutil
3
- from collections.abc import Generator
4
3
  from pathlib import Path
5
4
  from unittest.mock import MagicMock, patch
6
5
 
@@ -18,51 +17,6 @@ from plugin_runner.plugin_runner import (
18
17
  )
19
18
 
20
19
 
21
- @pytest.fixture
22
- def install_test_plugin(request: pytest.FixtureRequest) -> Generator[Path, None, None]:
23
- """Copies a specified plugin from the fixtures directory to the data directory
24
- and removes it after the test.
25
-
26
- Parameters:
27
- - request.param: The name of the plugin package to copy.
28
-
29
- Yields:
30
- - Path to the copied plugin directory.
31
- """
32
- # Define base directories
33
- base_dir = Path("./plugin_runner/tests")
34
- fixture_plugin_dir = base_dir / "fixtures" / "plugins"
35
- data_plugin_dir = base_dir / "data" / "plugins"
36
-
37
- # The plugin name should be passed as a parameter to the fixture
38
- plugin_name = request.param # Expected to be a str
39
- src_plugin_path = fixture_plugin_dir / plugin_name
40
- dest_plugin_path = data_plugin_dir / plugin_name
41
-
42
- # Ensure the data plugin directory exists
43
- data_plugin_dir.mkdir(parents=True, exist_ok=True)
44
-
45
- # Copy the specific plugin from fixtures to data
46
- try:
47
- shutil.copytree(src_plugin_path, dest_plugin_path)
48
- yield dest_plugin_path # Provide the path to the test
49
- finally:
50
- # Cleanup: remove data/plugins directory after the test
51
- if dest_plugin_path.exists():
52
- shutil.rmtree(dest_plugin_path)
53
-
54
-
55
- @pytest.fixture
56
- def load_test_plugins() -> Generator[None, None, None]:
57
- """Manages the lifecycle of test plugins by loading and unloading them."""
58
- try:
59
- load_plugins()
60
- yield
61
- finally:
62
- LOADED_PLUGINS.clear()
63
- EVENT_HANDLER_MAP.clear()
64
-
65
-
66
20
  @pytest.fixture
67
21
  def plugin_runner() -> PluginRunner:
68
22
  """Fixture to initialize PluginRunner with mocks."""
@@ -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 = """
@@ -33,6 +33,18 @@ import module.b
33
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 = """
settings.py CHANGED
@@ -72,3 +72,13 @@ PLUGIN_DIRECTORY = os.getenv(
72
72
  MANIFEST_FILE_NAME = "CANVAS_MANIFEST.json"
73
73
 
74
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
+ ]