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.
- {canvas-0.12.0.dist-info → canvas-0.13.1.dist-info}/METADATA +3 -1
- {canvas-0.12.0.dist-info → canvas-0.13.1.dist-info}/RECORD +31 -19
- canvas_cli/apps/plugin/plugin.py +13 -4
- canvas_cli/templates/plugins/application/cookiecutter.json +4 -0
- canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/CANVAS_MANIFEST.json +28 -0
- canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/README.md +11 -0
- canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/applications/__init__.py +0 -0
- canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/applications/my_application.py +12 -0
- canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/assets/python-logo.png +0 -0
- canvas_cli/utils/validators/manifest_schema.py +18 -1
- canvas_generated/messages/effects_pb2.py +2 -2
- canvas_generated/messages/effects_pb2.pyi +2 -0
- canvas_generated/messages/events_pb2.py +2 -2
- canvas_generated/messages/events_pb2.pyi +2 -0
- canvas_sdk/effects/__init__.py +3 -1
- canvas_sdk/effects/launch_modal.py +24 -0
- canvas_sdk/handlers/application.py +29 -0
- canvas_sdk/handlers/base.py +8 -4
- canvas_sdk/protocols/base.py +3 -1
- plugin_runner/exceptions.py +14 -0
- plugin_runner/plugin_installer.py +208 -0
- plugin_runner/plugin_runner.py +37 -23
- plugin_runner/plugin_synchronizer.py +10 -7
- plugin_runner/sandbox.py +2 -6
- plugin_runner/tests/test_application.py +65 -0
- plugin_runner/tests/test_plugin_installer.py +118 -0
- plugin_runner/tests/test_plugin_runner.py +4 -4
- plugin_runner/tests/test_sandbox.py +2 -2
- settings.py +5 -2
- {canvas-0.12.0.dist-info → canvas-0.13.1.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
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
|
|
147
|
+
assert EVENT_HANDLER_MAP == {}
|
|
148
148
|
load_plugins()
|
|
149
|
-
assert EventType.Name(EventType.UNKNOWN) in
|
|
150
|
-
assert
|
|
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,
|
|
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,
|
|
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()
|
|
File without changes
|
|
File without changes
|