canvas 0.3.1__py3-none-any.whl → 0.5.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.
- {canvas-0.3.1.dist-info → canvas-0.5.0.dist-info}/METADATA +4 -2
- {canvas-0.3.1.dist-info → canvas-0.5.0.dist-info}/RECORD +76 -37
- canvas_cli/apps/emit/emit.py +1 -1
- canvas_cli/apps/logs/logs.py +6 -6
- canvas_cli/apps/plugin/plugin.py +11 -7
- canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/protocols/my_protocol.py +3 -7
- canvas_cli/tests.py +12 -5
- canvas_cli/utils/context/context.py +2 -2
- canvas_cli/utils/context/tests.py +5 -4
- canvas_cli/utils/print/print.py +1 -1
- canvas_cli/utils/print/tests.py +2 -3
- canvas_generated/messages/events_pb2.py +2 -2
- canvas_generated/messages/events_pb2.pyi +112 -0
- canvas_sdk/base.py +2 -1
- canvas_sdk/commands/base.py +25 -25
- canvas_sdk/commands/tests/protocol/tests.py +5 -3
- canvas_sdk/commands/tests/test_utils.py +8 -44
- canvas_sdk/commands/tests/unit/tests.py +3 -3
- canvas_sdk/data/client.py +1 -1
- canvas_sdk/effects/banner_alert/tests.py +12 -4
- canvas_sdk/effects/patient_chart_summary_configuration.py +1 -0
- canvas_sdk/effects/protocol_card/protocol_card.py +1 -1
- canvas_sdk/effects/protocol_card/tests.py +2 -2
- canvas_sdk/protocols/clinical_quality_measure.py +1 -0
- canvas_sdk/utils/http.py +2 -2
- canvas_sdk/v1/data/common.py +46 -0
- canvas_sdk/v1/data/detected_issue.py +52 -0
- canvas_sdk/v1/data/imaging.py +102 -0
- canvas_sdk/v1/data/lab.py +182 -10
- canvas_sdk/v1/data/patient.py +4 -1
- canvas_sdk/v1/data/protocol_override.py +58 -0
- canvas_sdk/v1/data/questionnaire.py +4 -2
- canvas_sdk/value_set/__init__.py +0 -0
- canvas_sdk/value_set/tests/test_value_sets.py +9 -6
- canvas_sdk/value_set/v2022/__init__.py +0 -0
- canvas_sdk/value_set/v2022/intervention.py +0 -24
- canvas_sdk/value_set/value_set.py +24 -21
- plugin_runner/authentication.py +3 -7
- plugin_runner/plugin_runner.py +106 -33
- plugin_runner/sandbox.py +23 -9
- plugin_runner/tests/__init__.py +0 -0
- plugin_runner/tests/data/plugins/.gitkeep +0 -0
- plugin_runner/tests/fixtures/plugins/example_plugin/CANVAS_MANIFEST.json +29 -0
- plugin_runner/tests/fixtures/plugins/example_plugin/README.md +12 -0
- plugin_runner/tests/fixtures/plugins/example_plugin/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/example_plugin/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/example_plugin/protocols/my_protocol.py +18 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v1/CANVAS_MANIFEST.json +29 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v1/README.md +12 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v1/other_module/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v1/other_module/base.py +3 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v1/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v1/protocols/my_protocol.py +18 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v2/CANVAS_MANIFEST.json +29 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v2/README.md +12 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v2/other_module/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v2/other_module/base.py +6 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v2/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v2/protocols/my_protocol.py +18 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v3/CANVAS_MANIFEST.json +29 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v3/README.md +12 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v3/other_module/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v3/other_module/base.py +8 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v3/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v3/protocols/my_protocol.py +18 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_plugin/CANVAS_MANIFEST.json +29 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_plugin/README.md +12 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_plugin/other_module/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_plugin/other_module/base.py +3 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_plugin/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_plugin/protocols/my_protocol.py +18 -0
- plugin_runner/tests/test_plugin_runner.py +208 -0
- plugin_runner/tests/test_sandbox.py +113 -0
- settings.py +23 -0
- {canvas-0.3.1.dist-info → canvas-0.5.0.dist-info}/WHEEL +0 -0
- {canvas-0.3.1.dist-info → canvas-0.5.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,30 +1,6 @@
|
|
|
1
1
|
from ..value_set import ValueSet
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
class HospiceCareAmbulatory(ValueSet):
|
|
5
|
-
"""
|
|
6
|
-
**Clinical Focus:** The purpose of this value set is to represent concepts of interventions to identify patients receiving hospice care outside of a hospital or long term care facility.
|
|
7
|
-
|
|
8
|
-
**Data Element Scope:** This value set may use a model element related to Procedure or Intervention.
|
|
9
|
-
|
|
10
|
-
**Inclusion Criteria:** Includes concepts that represent a procedure or intervention for hospice care.
|
|
11
|
-
|
|
12
|
-
**Exclusion Criteria:** Excludes concepts that represent palliative care or comfort measures.
|
|
13
|
-
|
|
14
|
-
** Used in:** CMS90v11, CMS134v10, CMS165v10, CMS146v10, CMS124v10, CMS139v10, CMS154v10, CMS56v10, CMS74v11, CMS75v10, CMS137v10, CMS136v11, CMS128v10, CMS122v10, CMS153v10, CMS66v10, CMS130v10, CMS155v10, CMS127v10, CMS117v10, CMS131v10, CMS156v10, CMS125v10
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
VALUE_SET_NAME = "Hospice care ambulatory"
|
|
18
|
-
OID = "2.16.840.1.113762.1.4.1108.15"
|
|
19
|
-
DEFINITION_VERSION = "20170504"
|
|
20
|
-
EXPANSION_VERSION = "eCQM Update 2021-05-06"
|
|
21
|
-
|
|
22
|
-
SNOMEDCT = {
|
|
23
|
-
"385763009", # Hospice care (regime/therapy)
|
|
24
|
-
"385765002", # Hospice care management (procedure)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
4
|
class PalliativeCareIntervention(ValueSet):
|
|
29
5
|
"""
|
|
30
6
|
**Clinical Focus:** The purpose of this value set is to represent concepts for palliative care interventions.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from collections import defaultdict
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Union, cast
|
|
3
|
+
|
|
4
|
+
from django.utils.functional import classproperty
|
|
3
5
|
|
|
4
6
|
|
|
5
7
|
class CodeConstants:
|
|
@@ -33,7 +35,7 @@ class CodeConstants:
|
|
|
33
35
|
URL_NDC = "http://hl7.org/fhir/sid/ndc"
|
|
34
36
|
|
|
35
37
|
|
|
36
|
-
class
|
|
38
|
+
class CodeConstantsURLMappingMixin:
|
|
37
39
|
"""A class that maps code systems to their URLs."""
|
|
38
40
|
|
|
39
41
|
CODE_SYSTEM_MAPPING = {
|
|
@@ -53,21 +55,21 @@ class CodeConstantsURLMapping:
|
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
|
|
56
|
-
class CombinedValueSet(
|
|
58
|
+
class CombinedValueSet(CodeConstantsURLMappingMixin):
|
|
57
59
|
"""A class representing a combination of two value sets."""
|
|
58
60
|
|
|
59
61
|
def __init__(
|
|
60
62
|
self,
|
|
61
|
-
value_set_1: Union["ValueSet", "CombinedValueSet"],
|
|
62
|
-
value_set_2: Union["ValueSet", "CombinedValueSet"],
|
|
63
|
+
value_set_1: Union[type["ValueSet"], "CombinedValueSet"],
|
|
64
|
+
value_set_2: Union[type["ValueSet"], "CombinedValueSet"],
|
|
63
65
|
) -> None:
|
|
64
66
|
self.value_set_1 = value_set_1
|
|
65
67
|
self.value_set_2 = value_set_2
|
|
66
68
|
|
|
67
69
|
@property
|
|
68
|
-
def values(self) ->
|
|
70
|
+
def values(self) -> dict[str, set]:
|
|
69
71
|
"""A property that returns the combined values from both value sets."""
|
|
70
|
-
values:
|
|
72
|
+
values: dict[str, set] = defaultdict(set)
|
|
71
73
|
|
|
72
74
|
for vs in [self.value_set_1, self.value_set_2]:
|
|
73
75
|
sub_values = vs.values
|
|
@@ -77,7 +79,7 @@ class CombinedValueSet(CodeConstantsURLMapping):
|
|
|
77
79
|
|
|
78
80
|
return values
|
|
79
81
|
|
|
80
|
-
def __or__(self, value_set: Union["ValueSet", "CombinedValueSet"]) -> "CombinedValueSet":
|
|
82
|
+
def __or__(self, value_set: Union[type["ValueSet"], "CombinedValueSet"]) -> "CombinedValueSet":
|
|
81
83
|
"""Implements the `|` (or) operator to combine value sets."""
|
|
82
84
|
return CombinedValueSet(self, value_set)
|
|
83
85
|
|
|
@@ -85,22 +87,23 @@ class CombinedValueSet(CodeConstantsURLMapping):
|
|
|
85
87
|
class ValueSystems(type):
|
|
86
88
|
"""A metaclass for defining a ValueSet."""
|
|
87
89
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
""
|
|
91
|
-
return {
|
|
92
|
-
system: getattr(cls, system)
|
|
93
|
-
for system in cast(ValueSet, cls).CODE_SYSTEM_MAPPING.keys()
|
|
94
|
-
if hasattr(cls, system)
|
|
95
|
-
}
|
|
90
|
+
def __or__(self, value_set: Union[type["ValueSet"], "CombinedValueSet"]) -> "CombinedValueSet": # type: ignore[override]
|
|
91
|
+
"""Implements the `|` (or) operator."""
|
|
92
|
+
return CombinedValueSet(cast(type["ValueSet"], self), value_set)
|
|
96
93
|
|
|
97
|
-
def
|
|
94
|
+
def __ror__(self, value_set: Union[type["ValueSet"], "CombinedValueSet"]) -> "CombinedValueSet": # type: ignore[override]
|
|
98
95
|
"""Implements the `|` (or) operator."""
|
|
99
|
-
return
|
|
96
|
+
return self.__or__(value_set)
|
|
100
97
|
|
|
101
98
|
|
|
102
|
-
class ValueSet(
|
|
99
|
+
class ValueSet(CodeConstantsURLMappingMixin, metaclass=ValueSystems):
|
|
103
100
|
"""The Base class for a ValueSet."""
|
|
104
101
|
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
@classproperty
|
|
103
|
+
def values(cls) -> dict[str, set]:
|
|
104
|
+
"""A property that returns a dictionary of code systems and their associated values."""
|
|
105
|
+
return {
|
|
106
|
+
system: getattr(cls, system)
|
|
107
|
+
for system in cls.CODE_SYSTEM_MAPPING.keys()
|
|
108
|
+
if hasattr(cls, system)
|
|
109
|
+
}
|
plugin_runner/authentication.py
CHANGED
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
import os
|
|
2
|
-
from typing import cast
|
|
3
2
|
|
|
4
3
|
import arrow
|
|
5
4
|
from jwt import encode
|
|
6
5
|
|
|
7
6
|
from logger import log
|
|
7
|
+
from settings import PLUGIN_RUNNER_SIGNING_KEY
|
|
8
8
|
|
|
9
9
|
ONE_DAY_IN_MINUTES = 60 * 24
|
|
10
10
|
|
|
11
|
-
INSECURE_DEFAULT_SIGNING_KEY = "INSECURE_KEY"
|
|
12
|
-
|
|
13
11
|
|
|
14
12
|
def token_for_plugin(
|
|
15
13
|
plugin_name: str,
|
|
16
14
|
audience: str,
|
|
17
15
|
issuer: str = "plugin-runner",
|
|
18
|
-
jwt_signing_key: str =
|
|
19
|
-
str, os.getenv("PLUGIN_RUNNER_SIGNING_KEY", INSECURE_DEFAULT_SIGNING_KEY)
|
|
20
|
-
),
|
|
16
|
+
jwt_signing_key: str = PLUGIN_RUNNER_SIGNING_KEY,
|
|
21
17
|
expiration_minutes: int = ONE_DAY_IN_MINUTES,
|
|
22
18
|
extra_kwargs: dict | None = None,
|
|
23
19
|
) -> str:
|
|
@@ -27,7 +23,7 @@ def token_for_plugin(
|
|
|
27
23
|
if not extra_kwargs:
|
|
28
24
|
extra_kwargs = {}
|
|
29
25
|
|
|
30
|
-
if jwt_signing_key
|
|
26
|
+
if not jwt_signing_key:
|
|
31
27
|
log.warning(
|
|
32
28
|
"Using an insecure JWT signing key for GraphQL access. Set the PLUGIN_RUNNER_SIGNING_KEY environment variable to avoid this message."
|
|
33
29
|
)
|
plugin_runner/plugin_runner.py
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import importlib.util
|
|
3
2
|
import json
|
|
4
3
|
import os
|
|
5
4
|
import pathlib
|
|
5
|
+
import pkgutil
|
|
6
6
|
import signal
|
|
7
7
|
import sys
|
|
8
8
|
import time
|
|
9
9
|
import traceback
|
|
10
10
|
from collections import defaultdict
|
|
11
11
|
from types import FrameType
|
|
12
|
-
from typing import Any, Optional
|
|
12
|
+
from typing import Any, AsyncGenerator, Optional, TypedDict
|
|
13
13
|
|
|
14
14
|
import grpc
|
|
15
15
|
import statsd
|
|
@@ -30,17 +30,7 @@ from logger import log
|
|
|
30
30
|
from plugin_runner.authentication import token_for_plugin
|
|
31
31
|
from plugin_runner.plugin_synchronizer import publish_message
|
|
32
32
|
from plugin_runner.sandbox import Sandbox
|
|
33
|
-
|
|
34
|
-
ENV = os.getenv("ENV", "development")
|
|
35
|
-
|
|
36
|
-
IS_PRODUCTION = ENV == "production"
|
|
37
|
-
|
|
38
|
-
MANIFEST_FILE_NAME = "CANVAS_MANIFEST.json"
|
|
39
|
-
|
|
40
|
-
SECRETS_FILE_NAME = "SECRETS.json"
|
|
41
|
-
|
|
42
|
-
# specify a local plugin directory for development
|
|
43
|
-
PLUGIN_DIRECTORY = "/plugin-runner/custom-plugins" if IS_PRODUCTION else "./custom-plugins"
|
|
33
|
+
from settings import MANIFEST_FILE_NAME, PLUGIN_DIRECTORY, SECRETS_FILE_NAME
|
|
44
34
|
|
|
45
35
|
# when we import plugins we'll use the module name directly so we need to add the plugin
|
|
46
36
|
# directory to the path
|
|
@@ -51,7 +41,50 @@ sys.path.append(PLUGIN_DIRECTORY)
|
|
|
51
41
|
LOADED_PLUGINS: dict = {}
|
|
52
42
|
|
|
53
43
|
# a global dictionary of events to protocol class names
|
|
54
|
-
EVENT_PROTOCOL_MAP: dict =
|
|
44
|
+
EVENT_PROTOCOL_MAP: dict[str, list] = defaultdict(list)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class DataAccess(TypedDict):
|
|
48
|
+
"""DataAccess."""
|
|
49
|
+
|
|
50
|
+
event: str
|
|
51
|
+
read: list[str]
|
|
52
|
+
write: list[str]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
Protocol = TypedDict(
|
|
56
|
+
"Protocol",
|
|
57
|
+
{
|
|
58
|
+
"class": str,
|
|
59
|
+
"data_access": DataAccess,
|
|
60
|
+
},
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Components(TypedDict):
|
|
65
|
+
"""Components."""
|
|
66
|
+
|
|
67
|
+
protocols: list[Protocol]
|
|
68
|
+
commands: list[dict]
|
|
69
|
+
content: list[dict]
|
|
70
|
+
effects: list[dict]
|
|
71
|
+
views: list[dict]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class PluginManifest(TypedDict):
|
|
75
|
+
"""PluginManifest."""
|
|
76
|
+
|
|
77
|
+
sdk_version: str
|
|
78
|
+
plugin_version: str
|
|
79
|
+
name: str
|
|
80
|
+
description: str
|
|
81
|
+
components: Components
|
|
82
|
+
secrets: list[dict]
|
|
83
|
+
tags: dict[str, str]
|
|
84
|
+
references: list[str]
|
|
85
|
+
license: str
|
|
86
|
+
diagram: bool
|
|
87
|
+
readme: str
|
|
55
88
|
|
|
56
89
|
|
|
57
90
|
class PluginRunner(PluginRunnerServicer):
|
|
@@ -63,11 +96,19 @@ class PluginRunner(PluginRunnerServicer):
|
|
|
63
96
|
|
|
64
97
|
sandbox: Sandbox
|
|
65
98
|
|
|
66
|
-
async def HandleEvent(
|
|
99
|
+
async def HandleEvent(
|
|
100
|
+
self, request: Event, context: Any
|
|
101
|
+
) -> AsyncGenerator[EventResponse, None]:
|
|
67
102
|
"""This is invoked when an event comes in."""
|
|
68
103
|
event_start_time = time.time()
|
|
69
|
-
|
|
70
|
-
|
|
104
|
+
event_type = request.type
|
|
105
|
+
event_name = EventType.Name(event_type)
|
|
106
|
+
relevant_plugins = EVENT_PROTOCOL_MAP[event_name]
|
|
107
|
+
|
|
108
|
+
if event_type in [EventType.PLUGIN_CREATED, EventType.PLUGIN_UPDATED]:
|
|
109
|
+
plugin_name = request.target
|
|
110
|
+
# filter only for the plugin(s) that were created/updated
|
|
111
|
+
relevant_plugins = [p for p in relevant_plugins if p.startswith(f"{plugin_name}:")]
|
|
71
112
|
|
|
72
113
|
effect_list = []
|
|
73
114
|
|
|
@@ -129,7 +170,7 @@ class PluginRunner(PluginRunnerServicer):
|
|
|
129
170
|
|
|
130
171
|
async def ReloadPlugins(
|
|
131
172
|
self, request: ReloadPluginsRequest, context: Any
|
|
132
|
-
) -> ReloadPluginsResponse:
|
|
173
|
+
) -> AsyncGenerator[ReloadPluginsResponse, None]:
|
|
133
174
|
"""This is invoked when we need to reload plugins."""
|
|
134
175
|
try:
|
|
135
176
|
load_plugins()
|
|
@@ -146,18 +187,50 @@ def handle_hup_cb(_signum: int, _frame: Optional[FrameType]) -> None:
|
|
|
146
187
|
load_plugins()
|
|
147
188
|
|
|
148
189
|
|
|
149
|
-
def
|
|
190
|
+
def find_modules(base_path: pathlib.Path, prefix: str | None = None) -> list[str]:
|
|
191
|
+
"""Find all modules in the specified package path."""
|
|
192
|
+
modules: list[str] = []
|
|
193
|
+
|
|
194
|
+
for file_finder, module_name, is_pkg in pkgutil.iter_modules(
|
|
195
|
+
[base_path.as_posix()],
|
|
196
|
+
):
|
|
197
|
+
if is_pkg:
|
|
198
|
+
modules = modules + find_modules(
|
|
199
|
+
base_path / module_name,
|
|
200
|
+
prefix=f"{prefix}.{module_name}" if prefix else module_name,
|
|
201
|
+
)
|
|
202
|
+
else:
|
|
203
|
+
modules.append(f"{prefix}.{module_name}" if prefix else module_name)
|
|
204
|
+
|
|
205
|
+
return modules
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def sandbox_from_package(package_path: pathlib.Path) -> dict[str, Any]:
|
|
209
|
+
"""Sandbox the code execution."""
|
|
210
|
+
package_name = package_path.name
|
|
211
|
+
available_modules = find_modules(package_path)
|
|
212
|
+
sandboxes = {}
|
|
213
|
+
|
|
214
|
+
for module_name in available_modules:
|
|
215
|
+
result = sandbox_from_module(package_path, module_name)
|
|
216
|
+
full_module_name = f"{package_name}.{module_name}"
|
|
217
|
+
sandboxes[full_module_name] = result
|
|
218
|
+
|
|
219
|
+
return sandboxes
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def sandbox_from_module(package_path: pathlib.Path, module_name: str) -> Any:
|
|
150
223
|
"""Sandbox the code execution."""
|
|
151
|
-
|
|
224
|
+
module_path = package_path / str(module_name.replace(".", "/") + ".py")
|
|
152
225
|
|
|
153
|
-
if not
|
|
154
|
-
raise
|
|
226
|
+
if not module_path.exists():
|
|
227
|
+
raise ModuleNotFoundError(f'Could not load module "{module_name}"')
|
|
155
228
|
|
|
156
|
-
|
|
157
|
-
source_code = origin.read_text()
|
|
229
|
+
source_code = module_path.read_text()
|
|
158
230
|
|
|
159
|
-
|
|
231
|
+
full_module_name = f"{package_path.name}.{module_name}"
|
|
160
232
|
|
|
233
|
+
sandbox = Sandbox(source_code, module_name=full_module_name)
|
|
161
234
|
return sandbox.execute()
|
|
162
235
|
|
|
163
236
|
|
|
@@ -166,13 +239,13 @@ def load_or_reload_plugin(path: pathlib.Path) -> None:
|
|
|
166
239
|
log.info(f"Loading {path}")
|
|
167
240
|
|
|
168
241
|
manifest_file = path / MANIFEST_FILE_NAME
|
|
169
|
-
|
|
242
|
+
manifest_json_str = manifest_file.read_text()
|
|
170
243
|
|
|
171
244
|
# the name is the folder name underneath the plugins directory
|
|
172
245
|
name = path.name
|
|
173
246
|
|
|
174
247
|
try:
|
|
175
|
-
manifest_json = json.loads(
|
|
248
|
+
manifest_json: PluginManifest = json.loads(manifest_json_str)
|
|
176
249
|
except Exception as e:
|
|
177
250
|
log.error(f'Unable to load plugin "{name}": {e}')
|
|
178
251
|
return
|
|
@@ -189,6 +262,7 @@ def load_or_reload_plugin(path: pathlib.Path) -> None:
|
|
|
189
262
|
# TODO add existing schema validation from Michela here
|
|
190
263
|
try:
|
|
191
264
|
protocols = manifest_json["components"]["protocols"]
|
|
265
|
+
results = sandbox_from_package(path)
|
|
192
266
|
except Exception as e:
|
|
193
267
|
log.error(f'Unable to load plugin "{name}": {str(e)}')
|
|
194
268
|
return
|
|
@@ -207,7 +281,7 @@ def load_or_reload_plugin(path: pathlib.Path) -> None:
|
|
|
207
281
|
if name_and_class in LOADED_PLUGINS:
|
|
208
282
|
log.info(f"Reloading plugin '{name_and_class}'")
|
|
209
283
|
|
|
210
|
-
result =
|
|
284
|
+
result = results[protocol_module]
|
|
211
285
|
|
|
212
286
|
LOADED_PLUGINS[name_and_class]["active"] = True
|
|
213
287
|
|
|
@@ -217,7 +291,7 @@ def load_or_reload_plugin(path: pathlib.Path) -> None:
|
|
|
217
291
|
else:
|
|
218
292
|
log.info(f"Loading plugin '{name_and_class}'")
|
|
219
293
|
|
|
220
|
-
result =
|
|
294
|
+
result = results[protocol_module]
|
|
221
295
|
|
|
222
296
|
LOADED_PLUGINS[name_and_class] = {
|
|
223
297
|
"active": True,
|
|
@@ -234,8 +308,7 @@ def load_or_reload_plugin(path: pathlib.Path) -> None:
|
|
|
234
308
|
|
|
235
309
|
def refresh_event_type_map() -> None:
|
|
236
310
|
"""Ensure the event subscriptions are up to date."""
|
|
237
|
-
|
|
238
|
-
EVENT_PROTOCOL_MAP = defaultdict(list)
|
|
311
|
+
EVENT_PROTOCOL_MAP.clear()
|
|
239
312
|
|
|
240
313
|
for name, plugin in LOADED_PLUGINS.items():
|
|
241
314
|
if hasattr(plugin["class"], "RESPONDS_TO"):
|
|
@@ -264,8 +337,8 @@ def load_plugins(specified_plugin_paths: list[str] | None = None) -> None:
|
|
|
264
337
|
for plugin_path in plugin_paths:
|
|
265
338
|
# when we import plugins we'll use the module name directly so we need to add the plugin
|
|
266
339
|
# directory to the path
|
|
267
|
-
path_to_append =
|
|
268
|
-
sys.path.append(path_to_append)
|
|
340
|
+
path_to_append = pathlib.Path(".") / plugin_path.parent
|
|
341
|
+
sys.path.append(path_to_append.as_posix())
|
|
269
342
|
else:
|
|
270
343
|
candidates = os.listdir(PLUGIN_DIRECTORY)
|
|
271
344
|
|
plugin_runner/sandbox.py
CHANGED
|
@@ -75,12 +75,6 @@ def _is_known_module(name: str) -> bool:
|
|
|
75
75
|
return any(name.startswith(m) for m in ALLOWED_MODULES)
|
|
76
76
|
|
|
77
77
|
|
|
78
|
-
def _safe_import(name: str, *args: Any, **kwargs: Any) -> Any:
|
|
79
|
-
if not _is_known_module(name):
|
|
80
|
-
raise ImportError(f"{name!r} is not an allowed import.")
|
|
81
|
-
return __import__(name, *args, **kwargs)
|
|
82
|
-
|
|
83
|
-
|
|
84
78
|
def _unrestricted(_ob: Any, *args: Any, **kwargs: Any) -> Any:
|
|
85
79
|
"""Return the given object, unmodified."""
|
|
86
80
|
return _ob
|
|
@@ -96,6 +90,7 @@ class Sandbox:
|
|
|
96
90
|
|
|
97
91
|
source_code: str
|
|
98
92
|
namespace: str
|
|
93
|
+
module_name: str | None
|
|
99
94
|
|
|
100
95
|
class Transformer(RestrictingNodeTransformer):
|
|
101
96
|
"""A node transformer for customizing the sandbox compiler."""
|
|
@@ -113,7 +108,7 @@ class Sandbox:
|
|
|
113
108
|
=> 'from _a import x' is ok, because '_a' is not added to the scope.
|
|
114
109
|
"""
|
|
115
110
|
for name in node.names:
|
|
116
|
-
if "*" in name.name and not _is_known_module(node.module):
|
|
111
|
+
if "*" in name.name and node.module and not _is_known_module(node.module):
|
|
117
112
|
self.error(node, '"*" imports are not allowed.')
|
|
118
113
|
self.check_name(node, name.name)
|
|
119
114
|
if name.asname:
|
|
@@ -204,12 +199,20 @@ class Sandbox:
|
|
|
204
199
|
# Impossible Case only ctx Load, Store and Del are defined in ast.
|
|
205
200
|
raise NotImplementedError(f"Unknown ctx type: {type(node.ctx)}")
|
|
206
201
|
|
|
207
|
-
def __init__(
|
|
202
|
+
def __init__(
|
|
203
|
+
self, source_code: str, namespace: str | None = None, module_name: str | None = None
|
|
204
|
+
) -> None:
|
|
208
205
|
if source_code is None:
|
|
209
206
|
raise TypeError("source_code may not be None")
|
|
207
|
+
self.module_name = module_name
|
|
210
208
|
self.namespace = namespace or "protocols"
|
|
211
209
|
self.source_code = source_code
|
|
212
210
|
|
|
211
|
+
@cached_property
|
|
212
|
+
def package_name(self) -> str | None:
|
|
213
|
+
"""Return the root package name."""
|
|
214
|
+
return self.module_name.split(".")[0] if self.module_name else None
|
|
215
|
+
|
|
213
216
|
@cached_property
|
|
214
217
|
def scope(self) -> dict[str, Any]:
|
|
215
218
|
"""Return the scope used for evaluation."""
|
|
@@ -217,7 +220,7 @@ class Sandbox:
|
|
|
217
220
|
"__builtins__": {
|
|
218
221
|
**safe_builtins.copy(),
|
|
219
222
|
**utility_builtins.copy(),
|
|
220
|
-
"__import__": _safe_import,
|
|
223
|
+
"__import__": self._safe_import,
|
|
221
224
|
"classmethod": builtins.classmethod,
|
|
222
225
|
"staticmethod": builtins.staticmethod,
|
|
223
226
|
"any": builtins.any,
|
|
@@ -263,6 +266,17 @@ class Sandbox:
|
|
|
263
266
|
"""Return warnings encountered when compiling the source code."""
|
|
264
267
|
return cast(tuple[str, ...], self.compile_result.warnings)
|
|
265
268
|
|
|
269
|
+
def _is_known_module(self, name: str) -> bool:
|
|
270
|
+
return bool(
|
|
271
|
+
_is_known_module(name)
|
|
272
|
+
or (self.package_name and name.split(".")[0] == self.package_name)
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
def _safe_import(self, name: str, *args: Any, **kwargs: Any) -> Any:
|
|
276
|
+
if not (self._is_known_module(name)):
|
|
277
|
+
raise ImportError(f"{name!r} is not an allowed import.")
|
|
278
|
+
return __import__(name, *args, **kwargs)
|
|
279
|
+
|
|
266
280
|
def execute(self) -> dict:
|
|
267
281
|
"""Execute the given code in a restricted sandbox."""
|
|
268
282
|
if self.errors:
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sdk_version": "0.1.4",
|
|
3
|
+
"plugin_version": "0.0.1",
|
|
4
|
+
"name": "example_plugin",
|
|
5
|
+
"description": "Edit the description in CANVAS_MANIFEST.json",
|
|
6
|
+
"components": {
|
|
7
|
+
"protocols": [
|
|
8
|
+
{
|
|
9
|
+
"class": "example_plugin.protocols.my_protocol:Protocol",
|
|
10
|
+
"description": "A protocol that does xyz...",
|
|
11
|
+
"data_access": {
|
|
12
|
+
"event": "",
|
|
13
|
+
"read": [],
|
|
14
|
+
"write": []
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"commands": [],
|
|
19
|
+
"content": [],
|
|
20
|
+
"effects": [],
|
|
21
|
+
"views": []
|
|
22
|
+
},
|
|
23
|
+
"secrets": [],
|
|
24
|
+
"tags": {},
|
|
25
|
+
"references": [],
|
|
26
|
+
"license": "",
|
|
27
|
+
"diagram": false,
|
|
28
|
+
"readme": "./README.md"
|
|
29
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
==============
|
|
2
|
+
example_plugin
|
|
3
|
+
==============
|
|
4
|
+
|
|
5
|
+
## Description
|
|
6
|
+
|
|
7
|
+
A description of this plugin
|
|
8
|
+
|
|
9
|
+
### Important Note!
|
|
10
|
+
|
|
11
|
+
The CANVAS_MANIFEST.json is used when installing your plugin. Please ensure it
|
|
12
|
+
gets updated if you add, remove, or rename protocols.
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from canvas_sdk.effects import Effect, EffectType
|
|
2
|
+
from canvas_sdk.events import EventType
|
|
3
|
+
from canvas_sdk.protocols import BaseProtocol
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Protocol(BaseProtocol):
|
|
7
|
+
"""
|
|
8
|
+
You should put a helpful description of this protocol's behavior here.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# Name the event type you wish to run in response to
|
|
12
|
+
RESPONDS_TO = EventType.Name(EventType.UNKNOWN)
|
|
13
|
+
|
|
14
|
+
NARRATIVE_STRING = "I was inserted from my plugin's protocol."
|
|
15
|
+
|
|
16
|
+
def compute(self) -> list[Effect]:
|
|
17
|
+
"""This method gets called when an event of the type RESPONDS_TO is fired."""
|
|
18
|
+
return [Effect(type=EffectType.LOG, payload="Hello, world!")]
|
plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v1/CANVAS_MANIFEST.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sdk_version": "0.1.4",
|
|
3
|
+
"plugin_version": "0.0.1",
|
|
4
|
+
"name": "test_module_imports_outside_plugin_v1",
|
|
5
|
+
"description": "Edit the description in CANVAS_MANIFEST.json",
|
|
6
|
+
"components": {
|
|
7
|
+
"protocols": [
|
|
8
|
+
{
|
|
9
|
+
"class": "test_module_imports_outside_plugin_v1.protocols.my_protocol:Protocol",
|
|
10
|
+
"description": "A protocol that does xyz...",
|
|
11
|
+
"data_access": {
|
|
12
|
+
"event": "",
|
|
13
|
+
"read": [],
|
|
14
|
+
"write": []
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"commands": [],
|
|
19
|
+
"content": [],
|
|
20
|
+
"effects": [],
|
|
21
|
+
"views": []
|
|
22
|
+
},
|
|
23
|
+
"secrets": [],
|
|
24
|
+
"tags": {},
|
|
25
|
+
"references": [],
|
|
26
|
+
"license": "",
|
|
27
|
+
"diagram": false,
|
|
28
|
+
"readme": "./README.md"
|
|
29
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
==========================
|
|
2
|
+
test_module_imports_outside_plugin_v1
|
|
3
|
+
==========================
|
|
4
|
+
|
|
5
|
+
## Description
|
|
6
|
+
|
|
7
|
+
A description of this plugin
|
|
8
|
+
|
|
9
|
+
### Important Note!
|
|
10
|
+
|
|
11
|
+
The CANVAS_MANIFEST.json is used when installing your plugin. Please ensure it
|
|
12
|
+
gets updated if you add, remove, or rename protocols.
|
plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v1/other_module/__init__.py
ADDED
|
File without changes
|
plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v1/protocols/__init__.py
ADDED
|
File without changes
|
plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v1/protocols/my_protocol.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from test_module_imports_plugin.other_module.base import import_me
|
|
2
|
+
|
|
3
|
+
from canvas_sdk.effects import Effect, EffectType
|
|
4
|
+
from canvas_sdk.events import EventType
|
|
5
|
+
from canvas_sdk.protocols import BaseProtocol
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Protocol(BaseProtocol):
|
|
9
|
+
"""
|
|
10
|
+
You should put a helpful description of this protocol's behavior here.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
# Name the event type you wish to run in response to
|
|
14
|
+
RESPONDS_TO = EventType.Name(EventType.UNKNOWN)
|
|
15
|
+
|
|
16
|
+
def compute(self) -> list[Effect]:
|
|
17
|
+
"""This method gets called when an event of the type RESPONDS_TO is fired."""
|
|
18
|
+
return [Effect(type=EffectType.LOG, payload=import_me())]
|
plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v2/CANVAS_MANIFEST.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sdk_version": "0.1.4",
|
|
3
|
+
"plugin_version": "0.0.1",
|
|
4
|
+
"name": "test_module_imports_outside_plugin_v2",
|
|
5
|
+
"description": "Edit the description in CANVAS_MANIFEST.json",
|
|
6
|
+
"components": {
|
|
7
|
+
"protocols": [
|
|
8
|
+
{
|
|
9
|
+
"class": "test_module_imports_outside_plugin_v2.protocols.my_protocol:Protocol",
|
|
10
|
+
"description": "A protocol that does xyz...",
|
|
11
|
+
"data_access": {
|
|
12
|
+
"event": "",
|
|
13
|
+
"read": [],
|
|
14
|
+
"write": []
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"commands": [],
|
|
19
|
+
"content": [],
|
|
20
|
+
"effects": [],
|
|
21
|
+
"views": []
|
|
22
|
+
},
|
|
23
|
+
"secrets": [],
|
|
24
|
+
"tags": {},
|
|
25
|
+
"references": [],
|
|
26
|
+
"license": "",
|
|
27
|
+
"diagram": false,
|
|
28
|
+
"readme": "./README.md"
|
|
29
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
==========================
|
|
2
|
+
test_module_imports_outside_plugin_v2
|
|
3
|
+
==========================
|
|
4
|
+
|
|
5
|
+
## Description
|
|
6
|
+
|
|
7
|
+
A description of this plugin
|
|
8
|
+
|
|
9
|
+
### Important Note!
|
|
10
|
+
|
|
11
|
+
The CANVAS_MANIFEST.json is used when installing your plugin. Please ensure it
|
|
12
|
+
gets updated if you add, remove, or rename protocols.
|
plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v2/other_module/__init__.py
ADDED
|
File without changes
|