canvas 0.12.0__py3-none-any.whl → 0.13.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 (31) hide show
  1. {canvas-0.12.0.dist-info → canvas-0.13.1.dist-info}/METADATA +3 -1
  2. {canvas-0.12.0.dist-info → canvas-0.13.1.dist-info}/RECORD +31 -19
  3. canvas_cli/apps/plugin/plugin.py +13 -4
  4. canvas_cli/templates/plugins/application/cookiecutter.json +4 -0
  5. canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/CANVAS_MANIFEST.json +28 -0
  6. canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/README.md +11 -0
  7. canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/applications/__init__.py +0 -0
  8. canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/applications/my_application.py +12 -0
  9. canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/assets/python-logo.png +0 -0
  10. canvas_cli/utils/validators/manifest_schema.py +18 -1
  11. canvas_generated/messages/effects_pb2.py +2 -2
  12. canvas_generated/messages/effects_pb2.pyi +2 -0
  13. canvas_generated/messages/events_pb2.py +2 -2
  14. canvas_generated/messages/events_pb2.pyi +2 -0
  15. canvas_sdk/effects/__init__.py +3 -1
  16. canvas_sdk/effects/launch_modal.py +24 -0
  17. canvas_sdk/handlers/application.py +29 -0
  18. canvas_sdk/handlers/base.py +8 -4
  19. canvas_sdk/protocols/base.py +3 -1
  20. plugin_runner/exceptions.py +14 -0
  21. plugin_runner/plugin_installer.py +208 -0
  22. plugin_runner/plugin_runner.py +37 -23
  23. plugin_runner/plugin_synchronizer.py +10 -7
  24. plugin_runner/sandbox.py +2 -6
  25. plugin_runner/tests/test_application.py +65 -0
  26. plugin_runner/tests/test_plugin_installer.py +118 -0
  27. plugin_runner/tests/test_plugin_runner.py +4 -4
  28. plugin_runner/tests/test_sandbox.py +2 -2
  29. settings.py +5 -2
  30. {canvas-0.12.0.dist-info → canvas-0.13.1.dist-info}/WHEEL +0 -0
  31. {canvas-0.12.0.dist-info → canvas-0.13.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,118 @@
1
+ import json
2
+ import tarfile
3
+ import tempfile
4
+ from pathlib import Path
5
+ from unittest.mock import MagicMock
6
+
7
+ from pytest_mock import MockerFixture
8
+
9
+ import settings
10
+ from plugin_runner.plugin_installer import (
11
+ PluginAttributes,
12
+ _extract_rows_to_dict,
13
+ download_plugin,
14
+ install_plugins,
15
+ uninstall_plugin,
16
+ )
17
+
18
+
19
+ def _create_tarball(name: str) -> Path:
20
+ # Create a temporary tarball file
21
+ temp_dir = tempfile.mkdtemp()
22
+ tarball_path = Path(temp_dir) / f"{name}.tar.gz"
23
+
24
+ # Add some files to the tarball
25
+ with tarfile.open(tarball_path, "w:gz") as tar:
26
+ for i in range(3):
27
+ file_path = Path(temp_dir) / f"file{i}.txt"
28
+ file_path.write_text(f"Content of file {i}")
29
+ tar.add(file_path, arcname=f"file{i}.txt")
30
+
31
+ # Return a Path handle to the tarball
32
+ return tarball_path
33
+
34
+
35
+ def test_extract_rows_to_dict() -> None:
36
+ """Test that database rows can be extracted to a dictionary with secrets appropriately attributed to plugin."""
37
+ rows = [
38
+ {
39
+ "name": "plugin1",
40
+ "version": "1.0",
41
+ "package": "package1",
42
+ "key": "key1",
43
+ "value": "value1",
44
+ },
45
+ {
46
+ "name": "plugin1",
47
+ "version": "1.0",
48
+ "package": "package1",
49
+ "key": "key2",
50
+ "value": "value2",
51
+ },
52
+ {"name": "plugin2", "version": "2.0", "package": "package2", "key": None, "value": None},
53
+ ]
54
+
55
+ expected_output = {
56
+ "plugin1": {
57
+ "version": "1.0",
58
+ "package": "package1",
59
+ "secrets": {"key1": "value1", "key2": "value2"},
60
+ },
61
+ "plugin2": {
62
+ "version": "2.0",
63
+ "package": "package2",
64
+ "secrets": {},
65
+ },
66
+ }
67
+
68
+ result = _extract_rows_to_dict(rows)
69
+ assert result == expected_output
70
+
71
+
72
+ def test_plugin_installation_from_tarball(mocker: MockerFixture) -> None:
73
+ """Test that plugins can be installed from tarballs."""
74
+ mock_plugins = {
75
+ "plugin1": PluginAttributes(
76
+ version="1.0", package="plugins/plugin1.tar.gz", secrets={"key1": "value1"}
77
+ ),
78
+ "plugin2": PluginAttributes(
79
+ version="1.0", package="plugins/plugin2.tar", secrets={"key2": "value2"}
80
+ ),
81
+ }
82
+
83
+ tarball_1 = _create_tarball("plugin1")
84
+ tarball_2 = _create_tarball("plugin2")
85
+
86
+ mocker.patch("plugin_runner.plugin_installer.enabled_plugins", return_value=mock_plugins)
87
+ mocker.patch(
88
+ "plugin_runner.plugin_installer.download_plugin", side_effect=[tarball_1, tarball_2]
89
+ )
90
+
91
+ install_plugins()
92
+ assert Path("plugin_runner/tests/data/plugins/plugin1").exists()
93
+ assert Path("plugin_runner/tests/data/plugins/plugin1/SECRETS.json").exists()
94
+ with open("plugin_runner/tests/data/plugins/plugin1/SECRETS.json") as f:
95
+ assert json.load(f) == mock_plugins["plugin1"]["secrets"]
96
+ assert Path("plugin_runner/tests/data/plugins/plugin2").exists()
97
+ assert Path("plugin_runner/tests/data/plugins/plugin2/SECRETS.json").exists()
98
+ with open("plugin_runner/tests/data/plugins/plugin2/SECRETS.json") as f:
99
+ assert json.load(f) == mock_plugins["plugin2"]["secrets"]
100
+
101
+ uninstall_plugin("plugin1")
102
+ uninstall_plugin("plugin2")
103
+ assert not Path("plugin_runner/tests/data/plugins/plugin1").exists()
104
+ assert not Path("plugin_runner/tests/data/plugins/plugin2").exists()
105
+
106
+
107
+ def test_download(mocker: MockerFixture) -> None:
108
+ """Test that the plugin package can be written to disk, mocking out S3."""
109
+ mock_s3_client = MagicMock()
110
+ mocker.patch("boto3.client", return_value=mock_s3_client)
111
+
112
+ plugin_package = "plugins/plugin1.tar.gz"
113
+ with download_plugin(plugin_package) as plugin_path:
114
+ assert plugin_path.exists()
115
+
116
+ mock_s3_client.download_fileobj.assert_called_once_with(
117
+ "canvas-client-media", f"{settings.CUSTOMER_IDENTIFIER}/{plugin_package}", mocker.ANY
118
+ )
@@ -9,7 +9,7 @@ from canvas_generated.messages.effects_pb2 import EffectType
9
9
  from canvas_generated.messages.plugins_pb2 import ReloadPluginsRequest
10
10
  from canvas_sdk.events import EventRequest, EventType
11
11
  from plugin_runner.plugin_runner import (
12
- EVENT_PROTOCOL_MAP,
12
+ EVENT_HANDLER_MAP,
13
13
  LOADED_PLUGINS,
14
14
  PluginRunner,
15
15
  load_plugins,
@@ -144,10 +144,10 @@ def test_remove_plugin_should_be_removed_from_loaded_plugins(setup_test_plugin:
144
144
  @pytest.mark.parametrize("setup_test_plugin", ["example_plugin"], indirect=True)
145
145
  def test_load_plugins_should_refresh_event_protocol_map(setup_test_plugin: Path) -> None:
146
146
  """Test that the event protocol map is refreshed when loading plugins."""
147
- assert EVENT_PROTOCOL_MAP == {}
147
+ assert EVENT_HANDLER_MAP == {}
148
148
  load_plugins()
149
- assert EventType.Name(EventType.UNKNOWN) in EVENT_PROTOCOL_MAP
150
- assert EVENT_PROTOCOL_MAP[EventType.Name(EventType.UNKNOWN)] == [
149
+ assert EventType.Name(EventType.UNKNOWN) in EVENT_HANDLER_MAP
150
+ assert EVENT_HANDLER_MAP[EventType.Name(EventType.UNKNOWN)] == [
151
151
  "example_plugin:example_plugin.protocols.my_protocol:Protocol"
152
152
  ]
153
153
 
@@ -100,7 +100,7 @@ print("Hello, Sandbox!")
100
100
 
101
101
  def test_sandbox_module_name_imports_within_package() -> None:
102
102
  """Test that modules within the same package can be imported."""
103
- sandbox_module_a = Sandbox(source_code=SOURCE_CODE_MODULE_OS, module_name="os.a")
103
+ sandbox_module_a = Sandbox(source_code=SOURCE_CODE_MODULE_OS, namespace="os.a")
104
104
  result = sandbox_module_a.execute()
105
105
 
106
106
  assert "os" in result
@@ -108,6 +108,6 @@ def test_sandbox_module_name_imports_within_package() -> None:
108
108
 
109
109
  def test_sandbox_denies_module_name_import_outside_package() -> None:
110
110
  """Test that modules outside the root package cannot be imported."""
111
- sandbox_module_a = Sandbox(source_code=SOURCE_CODE_MODULE_OS, module_name="module.a")
111
+ sandbox_module_a = Sandbox(source_code=SOURCE_CODE_MODULE_OS, namespace="module.a")
112
112
  with pytest.raises(ImportError, match="os' is not an allowed import."):
113
113
  sandbox_module_a.execute()
settings.py CHANGED
@@ -2,16 +2,18 @@ import os
2
2
  import sys
3
3
 
4
4
  from dotenv import load_dotenv
5
- from env_tools import env_to_bool
5
+ from env_tools import env_to_bool, get_enforcement_context
6
6
 
7
7
  from canvas_sdk.utils.db import get_database_dict_from_url
8
8
 
9
+ require_env, enforce_required_envs = get_enforcement_context()
10
+
9
11
  load_dotenv()
10
12
 
11
13
  ENV = os.getenv("ENV", "development")
12
14
  IS_PRODUCTION = ENV == "production"
13
15
  IS_TESTING = env_to_bool("IS_TESTING", "pytest" in sys.argv[0] or sys.argv[0] == "-c")
14
-
16
+ CUSTOMER_IDENTIFIER = require_env("CUSTOMER_IDENTIFIER")
15
17
 
16
18
  INTEGRATION_TEST_URL = os.getenv("INTEGRATION_TEST_URL")
17
19
  INTEGRATION_TEST_CLIENT_ID = os.getenv("INTEGRATION_TEST_CLIENT_ID")
@@ -66,3 +68,4 @@ PLUGIN_DIRECTORY = os.getenv(
66
68
  MANIFEST_FILE_NAME = "CANVAS_MANIFEST.json"
67
69
 
68
70
  SECRETS_FILE_NAME = "SECRETS.json"
71
+ enforce_required_envs()