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
plugin_runner/sandbox.py CHANGED
@@ -1,12 +1,18 @@
1
+ from __future__ import annotations
2
+
1
3
  import ast
2
4
  import builtins
3
5
  import importlib
6
+ import pkgutil
4
7
  import sys
8
+ import types
5
9
  from _ast import AnnAssign
10
+ from collections.abc import Iterable, Sequence
6
11
  from functools import cached_property
7
12
  from pathlib import Path
8
- from typing import Any, cast
13
+ from typing import TYPE_CHECKING, Any, TypedDict, cast
9
14
 
15
+ from frozendict import frozendict
10
16
  from RestrictedPython import (
11
17
  CompileResult,
12
18
  PrintCollector,
@@ -15,7 +21,6 @@ from RestrictedPython import (
15
21
  safe_builtins,
16
22
  utility_builtins,
17
23
  )
18
- from RestrictedPython.Eval import default_guarded_getitem
19
24
  from RestrictedPython.Guards import (
20
25
  guarded_iter_unpack_sequence,
21
26
  guarded_unpack_sequence,
@@ -23,78 +28,259 @@ from RestrictedPython.Guards import (
23
28
  from RestrictedPython.transformer import (
24
29
  ALLOWED_FUNC_NAMES,
25
30
  FORBIDDEN_FUNC_NAMES,
31
+ INSPECT_ATTRIBUTES,
26
32
  copy_locations,
27
33
  )
28
34
 
29
- ##
30
- # ALLOWED_MODULES
31
- #
32
- # The modules in this list are the only ones that can be imported in a sandboxed
33
- # runtime.
34
- #
35
- ALLOWED_MODULES = frozenset(
36
- [
37
- "__future__",
38
- "_strptime",
39
- "arrow",
40
- "base64",
41
- "cached_property",
42
- "canvas_sdk.commands",
43
- "canvas_sdk.data",
44
- "canvas_sdk.effects",
45
- "canvas_sdk.events",
46
- "canvas_sdk.handlers",
47
- "canvas_sdk.protocols",
48
- "canvas_sdk.questionnaires",
49
- "canvas_sdk.utils",
50
- "canvas_sdk.templates",
51
- "canvas_sdk.v1",
52
- "canvas_sdk.value_set",
53
- "canvas_sdk.views",
54
- "contextlib",
55
- "dataclasses",
56
- "datetime",
57
- "dateutil",
58
- "decimal",
59
- "django.db.models",
60
- "django.utils.functional",
61
- "enum",
62
- "functools",
63
- "hashlib",
64
- "hmac",
65
- "http",
66
- "json",
67
- "jwt",
68
- "logger",
69
- "math",
70
- "operator",
71
- "pickletools",
72
- "pydantic",
73
- "random",
74
- "rapidfuzz",
75
- "re",
76
- "requests",
77
- "secrets",
78
- "string",
79
- "time",
80
- "traceback",
81
- "typing",
82
- "urllib",
83
- "uuid",
84
- ]
35
+ if TYPE_CHECKING:
36
+
37
+ class ImportedNames(TypedDict):
38
+ """
39
+ Type the stored imported_names dicitionary for mypy.
40
+ """
41
+
42
+ names: list[str]
43
+ names_to_module: dict[str, str]
44
+
45
+
46
+ def find_submodules(starting_modules: Iterable[str]) -> list[str]:
47
+ """
48
+ Given a list of modules, return a list of those modules and their submodules.
49
+ """
50
+ submodules = set(starting_modules)
51
+
52
+ for module_path in starting_modules:
53
+ try:
54
+ module = importlib.import_module(module_path)
55
+
56
+ if not hasattr(module, "__path__"):
57
+ continue
58
+
59
+ for _, name, _ in pkgutil.walk_packages(module.__path__, prefix=module.__name__ + "."):
60
+ submodules.add(name)
61
+ except Exception as e:
62
+ print(f"could not import {module_path}: {e}")
63
+
64
+ return sorted(submodules)
65
+
66
+
67
+ SAFE_INTERNAL_DUNDER_READ_ATTRIBUTES = {
68
+ "__class__",
69
+ "__dict__",
70
+ "__eq__",
71
+ "__init__",
72
+ "__name__",
73
+ }
74
+
75
+
76
+ SAFE_EXTERNAL_DUNDER_READ_ATTRIBUTES = {
77
+ "__dict__",
78
+ "__eq__",
79
+ "__init__",
80
+ "__name__",
81
+ }
82
+
83
+ CANVAS_TOP_LEVEL_MODULES = (
84
+ "canvas_sdk.commands",
85
+ "canvas_sdk.effects",
86
+ "canvas_sdk.events",
87
+ "canvas_sdk.handlers",
88
+ "canvas_sdk.protocols",
89
+ "canvas_sdk.questionnaires",
90
+ "canvas_sdk.templates",
91
+ "canvas_sdk.utils",
92
+ "canvas_sdk.v1",
93
+ "canvas_sdk.value_set",
94
+ "canvas_sdk.views",
95
+ "logger",
85
96
  )
86
97
 
98
+ CANVAS_SUBMODULE_NAMES = [
99
+ found_module
100
+ for found_module in find_submodules(CANVAS_TOP_LEVEL_MODULES)
101
+ # tests are excluded from the built and distributed module in pyproject.toml
102
+ if "tests" not in found_module and "test_" not in found_module
103
+ ]
104
+
105
+ CANVAS_MODULES: dict[str, set[str]] = {}
106
+
107
+ for module_name in CANVAS_SUBMODULE_NAMES:
108
+ module = importlib.import_module(module_name)
109
+
110
+ exports = getattr(module, "__exports__", None)
111
+
112
+ if not exports:
113
+ continue
114
+
115
+ if module_name not in CANVAS_MODULES:
116
+ CANVAS_MODULES[module_name] = set()
87
117
 
88
- ##
89
- # FORBIDDEN_ASSIGNMENTS
90
- #
91
- # The names in this list are forbidden to be assigned to in a sandboxed runtime.
92
- #
93
- FORBIDDEN_ASSIGNMENTS = frozenset(["__name__", "__is_plugin__"])
118
+ CANVAS_MODULES[module_name].update(exports)
119
+
120
+ # In use by a current plugin...
121
+ CANVAS_MODULES["canvas_sdk.commands"].add("*")
122
+
123
+
124
+ STANDARD_LIBRARY_MODULES = {
125
+ "__future__": {
126
+ "annotations",
127
+ },
128
+ "_strptime": set(), # gets imported at runtime via datetime.datetime.strptime()
129
+ "base64": {
130
+ "b64decode",
131
+ "b64encode",
132
+ },
133
+ "datetime": {
134
+ "date",
135
+ "datetime",
136
+ "timedelta",
137
+ "timezone",
138
+ "UTC",
139
+ },
140
+ "dateutil": {
141
+ "relativedelta",
142
+ },
143
+ "dateutil.relativedelta": {
144
+ "relativedelta",
145
+ },
146
+ "decimal": {
147
+ "Decimal",
148
+ },
149
+ "enum": {
150
+ "Enum",
151
+ "StrEnum",
152
+ },
153
+ "functools": {
154
+ "reduce",
155
+ },
156
+ "hashlib": {
157
+ "sha256",
158
+ },
159
+ "hmac": {
160
+ "compare_digest",
161
+ "new",
162
+ },
163
+ "http": {
164
+ "HTTPStatus",
165
+ },
166
+ "json": {
167
+ "dumps",
168
+ "loads",
169
+ },
170
+ "operator": {
171
+ "and_",
172
+ },
173
+ "random": {
174
+ "choices",
175
+ "uniform",
176
+ "randint",
177
+ },
178
+ "re": {
179
+ "compile",
180
+ "DOTALL",
181
+ "IGNORECASE",
182
+ "match",
183
+ "search",
184
+ "split",
185
+ "sub",
186
+ },
187
+ "string": {
188
+ "ascii_lowercase",
189
+ "digits",
190
+ },
191
+ "time": {
192
+ "time",
193
+ "sleep",
194
+ },
195
+ "typing": {
196
+ "Any",
197
+ "Dict",
198
+ "Final",
199
+ "Iterable",
200
+ "List",
201
+ "NamedTuple",
202
+ "NotRequired",
203
+ "Protocol",
204
+ "Sequence",
205
+ "Tuple",
206
+ "Type",
207
+ "TypedDict",
208
+ },
209
+ "urllib.parse": {
210
+ "urlencode",
211
+ "quote",
212
+ },
213
+ "uuid": {
214
+ "uuid4",
215
+ "UUID",
216
+ },
217
+ }
218
+
219
+
220
+ THIRD_PARTY_MODULES = {
221
+ "arrow": {
222
+ "get",
223
+ "now",
224
+ "utcnow",
225
+ },
226
+ "django.db.models": {
227
+ "BigIntegerField",
228
+ "Case",
229
+ "CharField",
230
+ "IntegerField",
231
+ "Model", # remove when hyperscribe no longer needs it
232
+ "Q",
233
+ "Value",
234
+ "When",
235
+ },
236
+ "django.db.models.expressions": {
237
+ "Case",
238
+ "Value",
239
+ "When",
240
+ },
241
+ "django.db.models.query": {
242
+ "QuerySet",
243
+ },
244
+ "django.utils.functional": {
245
+ "cached_property",
246
+ },
247
+ "jwt": {
248
+ "decode",
249
+ "encode",
250
+ },
251
+ "pydantic": {
252
+ "ValidationError",
253
+ },
254
+ "rapidfuzz": {
255
+ "fuzz",
256
+ "process",
257
+ "utils",
258
+ },
259
+ "requests": {
260
+ "delete",
261
+ "get",
262
+ "patch",
263
+ "post",
264
+ "put",
265
+ "request",
266
+ "RequestException",
267
+ "Response",
268
+ },
269
+ }
270
+
271
+
272
+ # The modules in this list are the only ones that can be imported in a sandboxed runtime.
273
+ ALLOWED_MODULES = frozendict(
274
+ {
275
+ **CANVAS_MODULES,
276
+ **STANDARD_LIBRARY_MODULES,
277
+ **THIRD_PARTY_MODULES,
278
+ }
279
+ )
94
280
 
95
281
 
96
282
  def _is_known_module(name: str) -> bool:
97
- return any(name.startswith(m) for m in ALLOWED_MODULES)
283
+ return name in ALLOWED_MODULES
98
284
 
99
285
 
100
286
  def _unrestricted(_ob: Any, *args: Any, **kwargs: Any) -> Any:
@@ -108,7 +294,9 @@ def _apply(_ob: Any, *args: Any, **kwargs: Any) -> Any:
108
294
 
109
295
 
110
296
  def _find_folder_in_path(file_path: Path, target_folder_name: str) -> Path | None:
111
- """Recursively search for a folder with the specified name in the hierarchy of the given file path."""
297
+ """
298
+ Recursively search for a folder with the specified name in the hierarchy of the given file path.
299
+ """
112
300
  file_path = file_path.resolve()
113
301
 
114
302
  if file_path.name == target_folder_name:
@@ -130,6 +318,50 @@ class Sandbox:
130
318
  class Transformer(RestrictingNodeTransformer):
131
319
  """A node transformer for customizing the sandbox compiler."""
132
320
 
321
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
322
+ super().__init__(*args, **kwargs)
323
+
324
+ # we can't just add a self attribute here so we abuse used_names
325
+ # which gets returned as part of the CompileResult
326
+ self.used_names["__imported_names__"] = {
327
+ "names": [],
328
+ "names_to_module": {},
329
+ }
330
+
331
+ def handle_names(self, node: ast.Import | ast.ImportFrom) -> None:
332
+ """
333
+ Store imported names.
334
+ """
335
+ module = node.module if isinstance(node, ast.ImportFrom) else None
336
+
337
+ for name in node.names:
338
+ name_string = name.asname if name.asname else name.name
339
+
340
+ self.used_names["__imported_names__"]["names"].append(name_string)
341
+
342
+ if module:
343
+ self.used_names["__imported_names__"]["names_to_module"][name_string] = module
344
+
345
+ def visit_Import(self, node: ast.Import) -> ast.Import:
346
+ """
347
+ Store imported names.
348
+ """
349
+ node = super().visit_Import(node)
350
+
351
+ self.handle_names(node)
352
+
353
+ return node
354
+
355
+ def visit_ImportFrom(self, node: ast.ImportFrom) -> ast.ImportFrom:
356
+ """
357
+ Store imported names.
358
+ """
359
+ node = super().visit_ImportFrom(node)
360
+
361
+ self.handle_names(node)
362
+
363
+ return node
364
+
133
365
  def visit_AnnAssign(self, node: AnnAssign) -> AnnAssign:
134
366
  """Allow type annotations."""
135
367
  return node
@@ -145,7 +377,9 @@ class Sandbox:
145
377
  for name in node.names:
146
378
  if "*" in name.name and node.module and not _is_known_module(node.module):
147
379
  self.error(node, '"*" imports are not allowed.')
380
+
148
381
  self.check_name(node, name.name)
382
+
149
383
  if name.asname:
150
384
  self.check_name(node, name.asname)
151
385
 
@@ -189,7 +423,11 @@ class Sandbox:
189
423
  def visit_Assign(self, node: ast.Assign) -> ast.AST:
190
424
  """Check for forbidden assignments."""
191
425
  for target in node.targets:
192
- if isinstance(target, ast.Name) and target.id in FORBIDDEN_ASSIGNMENTS:
426
+ if (
427
+ isinstance(target, ast.Name)
428
+ and target.id.startswith("__")
429
+ and target.id != "__all__"
430
+ ):
193
431
  self.error(node, f"Assignments to '{target.id}' are not allowed.")
194
432
  elif isinstance(target, ast.Tuple | ast.List):
195
433
  self.check_for_name_in_iterable(target)
@@ -199,7 +437,7 @@ class Sandbox:
199
437
  def check_for_name_in_iterable(self, iterable_node: ast.Tuple | ast.List) -> None:
200
438
  """Check if any element of an iterable is a forbidden assignment."""
201
439
  for elt in iterable_node.elts:
202
- if isinstance(elt, ast.Name) and elt.id in FORBIDDEN_ASSIGNMENTS:
440
+ if isinstance(elt, ast.Name) and elt.id.startswith("__") and elt.id != "__all__":
203
441
  self.error(iterable_node, f"Assignments to '{elt.id}' are not allowed.")
204
442
  elif isinstance(elt, ast.Tuple | ast.List):
205
443
  self.check_for_name_in_iterable(elt)
@@ -240,8 +478,15 @@ class Sandbox:
240
478
 
241
479
  elif isinstance(node.ctx, ast.Store | ast.Del):
242
480
  node = self.node_contents_visit(node)
481
+
243
482
  new_value = ast.Call(
244
- func=ast.Name("_write_", ast.Load()), args=[node.value], keywords=[]
483
+ func=ast.Name("_write_", ast.Load()),
484
+ args=[
485
+ node.value,
486
+ ast.Constant(node.value.id if isinstance(node.value, ast.Name) else None),
487
+ ast.Constant(node.attr),
488
+ ],
489
+ keywords=[],
245
490
  )
246
491
 
247
492
  copy_locations(new_value, node.value)
@@ -254,26 +499,21 @@ class Sandbox:
254
499
 
255
500
  def __init__(
256
501
  self,
257
- source_code: str | Path | None,
258
- namespace: str | None = None,
502
+ source_code: Path,
503
+ namespace: str,
259
504
  evaluated_modules: dict[str, bool] | None = None,
260
505
  ) -> None:
261
- if source_code is None:
262
- raise TypeError("source_code may not be None")
263
-
264
506
  self.namespace = namespace or "protocols"
265
507
  self.package_name = self.namespace.split(".")[0]
266
508
 
267
- if isinstance(source_code, Path):
268
- if not source_code.exists():
269
- raise FileNotFoundError(f"File not found: {source_code}")
270
- self.source_code = source_code.read_text()
271
- package_path = _find_folder_in_path(source_code, self.package_name)
272
- self.base_path = package_path.parent if package_path else None
273
- self._evaluated_modules: dict[str, bool] = evaluated_modules or {}
274
- else:
275
- self.source_code = source_code
276
- self.base_path = None
509
+ if not source_code.exists():
510
+ raise FileNotFoundError(f"File not found: {source_code}")
511
+
512
+ self.source_code_path = source_code.as_posix()
513
+ self.source_code = source_code.read_text()
514
+ package_path = _find_folder_in_path(source_code, self.package_name)
515
+ self.base_path = package_path.parent if package_path else None
516
+ self._evaluated_modules: dict[str, bool] = evaluated_modules or {}
277
517
 
278
518
  @cached_property
279
519
  def scope(self) -> dict[str, Any]:
@@ -283,41 +523,51 @@ class Sandbox:
283
523
  **safe_builtins.copy(),
284
524
  **utility_builtins.copy(),
285
525
  "__import__": self._safe_import,
286
- "classmethod": builtins.classmethod,
287
- "staticmethod": builtins.staticmethod,
288
- "any": builtins.any,
289
526
  "all": builtins.all,
290
- "enumerate": builtins.enumerate,
291
- "property": builtins.property,
292
- "super": builtins.super,
527
+ "any": builtins.any,
528
+ "classmethod": builtins.classmethod,
293
529
  "dict": builtins.dict,
530
+ "enumerate": builtins.enumerate,
294
531
  "filter": builtins.filter,
532
+ "hasattr": builtins.hasattr,
533
+ "iter": builtins.iter,
534
+ "list": builtins.list,
535
+ "map": builtins.map,
295
536
  "max": builtins.max,
296
537
  "min": builtins.min,
297
- "list": builtins.list,
298
538
  "next": builtins.next,
299
- "iter": builtins.iter,
300
- "type": builtins.type,
539
+ "property": builtins.property,
540
+ "reversed": builtins.reversed,
541
+ "staticmethod": builtins.staticmethod,
542
+ "super": builtins.super,
301
543
  },
544
+ "__is_plugin__": True,
302
545
  "__metaclass__": type,
303
546
  "__name__": self.namespace,
304
- "__is_plugin__": True,
305
- "_write_": _unrestricted,
306
- "_getiter_": _unrestricted,
307
- "_getitem_": default_guarded_getitem,
308
- "_getattr_": getattr,
309
- "_print_": PrintCollector,
310
547
  "_apply_": _apply,
548
+ "_getattr_": self._safe_getattr,
549
+ "_getitem_": self._safe_getitem,
550
+ "_getiter_": _unrestricted,
311
551
  "_inplacevar_": _unrestricted,
312
552
  "_iter_unpack_sequence_": guarded_iter_unpack_sequence,
553
+ "_print_": PrintCollector,
313
554
  "_unpack_sequence_": guarded_unpack_sequence,
314
- "hasattr": hasattr,
555
+ "_write_": self._safe_write,
315
556
  }
316
557
 
317
558
  @cached_property
318
559
  def compile_result(self) -> CompileResult:
319
560
  """Compile the source code into bytecode."""
320
- return compile_restricted_exec(self.source_code, policy=self.Transformer)
561
+ return compile_restricted_exec(
562
+ source=self.source_code,
563
+ policy=self.Transformer,
564
+ filename=self.source_code_path,
565
+ )
566
+
567
+ @property
568
+ def imported_names(self) -> ImportedNames:
569
+ """Return the imported names collecting during parsing."""
570
+ return self.compile_result.used_names["__imported_names__"]
321
571
 
322
572
  @property
323
573
  def errors(self) -> tuple[str, ...]:
@@ -330,10 +580,7 @@ class Sandbox:
330
580
  return cast(tuple[str, ...], self.compile_result.warnings)
331
581
 
332
582
  def _is_known_module(self, name: str) -> bool:
333
- return bool(
334
- _is_known_module(name)
335
- or (self.package_name and name.split(".")[0] == self.package_name and self.base_path)
336
- )
583
+ return _is_known_module(name) or self._same_module(name)
337
584
 
338
585
  def _get_module(self, module_name: str) -> Path:
339
586
  """Get the module path for the given module name."""
@@ -347,10 +594,13 @@ class Sandbox:
347
594
 
348
595
  def _evaluate_module(self, module_name: str) -> None:
349
596
  """Evaluate the given module in the sandbox.
350
- If the module to import belongs to the same package as the current module, evaluate it inside a sandbox.
597
+
598
+ If the module to import belongs to the same package as the current module,
599
+ evaluate it inside a sandbox.
351
600
  """
352
- if not module_name.startswith(self.package_name) or module_name in self._evaluated_modules:
353
- return # Skip modules outside the package or already evaluated.
601
+ # Skip modules already evaluated
602
+ if not self._same_module(module_name) or module_name in self._evaluated_modules:
603
+ return
354
604
 
355
605
  module = self._get_module(module_name)
356
606
  self._evaluate_implicit_imports(module)
@@ -358,8 +608,11 @@ class Sandbox:
358
608
  # Re-check after evaluating implicit imports to avoid duplicate evaluations.
359
609
  if module_name not in self._evaluated_modules:
360
610
  Sandbox(
361
- module, namespace=module_name, evaluated_modules=self._evaluated_modules
611
+ module,
612
+ namespace=module_name,
613
+ evaluated_modules=self._evaluated_modules,
362
614
  ).execute()
615
+
363
616
  self._evaluated_modules[module_name] = True
364
617
 
365
618
  # Reload the module if already imported to ensure the latest version is used.
@@ -372,7 +625,8 @@ class Sandbox:
372
625
  parent = module.parent.parent if module.name == "__init__.py" else module.parent
373
626
  base_path = cast(Path, self.base_path)
374
627
 
375
- # Skip evaluation if the parent module is outside the base path or already the source code root.
628
+ # Skip evaluation if the parent module is outside the base path or
629
+ # already the source code root.
376
630
  if not parent.is_relative_to(base_path) or parent == base_path:
377
631
  return
378
632
 
@@ -383,8 +637,11 @@ class Sandbox:
383
637
  if init_file.exists():
384
638
  # Mark as evaluated to prevent infinite recursion.
385
639
  self._evaluated_modules[module_name] = True
640
+
386
641
  Sandbox(
387
- init_file, namespace=module_name, evaluated_modules=self._evaluated_modules
642
+ init_file,
643
+ namespace=module_name,
644
+ evaluated_modules=self._evaluated_modules,
388
645
  ).execute()
389
646
  else:
390
647
  # Mark as evaluated even if no init file exists to prevent redundant checks.
@@ -392,13 +649,126 @@ class Sandbox:
392
649
 
393
650
  self._evaluate_implicit_imports(parent)
394
651
 
395
- def _safe_import(self, name: str, *args: Any, **kwargs: Any) -> Any:
396
- if not self._is_known_module(name):
397
- raise ImportError(f"{name!r} is not an allowed import.")
652
+ def _same_module(self, module: str) -> bool:
653
+ """
654
+ Return True if `module` is within the plugin code.
655
+ """
656
+ return bool(self.base_path) and module.split(".")[0] == self.package_name
657
+
658
+ def _safe_write(self, _ob: Any, name: str | None = None, attribute: str | None = None) -> Any:
659
+ """Check if the given obj belongs to a protected resource."""
660
+ is_module = isinstance(_ob, types.ModuleType)
661
+
662
+ if is_module:
663
+ if not self._same_module(_ob.__name__):
664
+ raise AttributeError(f"Forbidden assignment to a module attribute: {_ob.__name__}.")
665
+ elif isinstance(_ob, type):
666
+ full_name = f"{_ob.__module__}.{_ob.__qualname__}"
667
+ else:
668
+ full_name = f"{_ob.__module__}.{_ob.__class__.__qualname__}"
669
+
670
+ if not self._same_module(_ob.__module__) and (
671
+ # deny if it was anything imported
672
+ name in self.imported_names["names"]
673
+ # deny if it's anything callable
674
+ or (attribute is not None and callable(getattr(_ob, attribute)))
675
+ ):
676
+ raise AttributeError(
677
+ f"Forbidden assignment to a non-module attribute: {full_name} "
678
+ f"at {name}.{attribute}."
679
+ )
680
+
681
+ return _ob
682
+
683
+ def _safe_getitem(self, ob: Any, index: Any) -> Any:
684
+ """
685
+ Prevent access to several classes of items.
686
+ """
687
+ if isinstance(index, str) and index.startswith("_"):
688
+ raise AttributeError(f'"{index}" is an invalid item name because it starts with "_"')
689
+
690
+ return ob[index]
691
+
692
+ def _safe_getattr(self, _ob: Any, name: Any, default: Any = None) -> Any:
693
+ """
694
+ Prevent access to several classes of attributes.
695
+
696
+ Restricted attribute types:
697
+
698
+ 1. underscored attributes created outside of the defining namespace
699
+ 2. attributes used by the `inspect` module
700
+ 3. dunder methods except for those we deem safe
701
+ 4. if a __exports__ module property is defined, any
702
+ attribute not in that property's value
703
+ """
704
+ is_module = isinstance(_ob, types.ModuleType)
398
705
 
706
+ if is_module:
707
+ module = _ob.__name__.split(".")[0]
708
+ elif isinstance(_ob, type):
709
+ module = _ob.__module__.split(".")[0]
710
+ else:
711
+ module = _ob.__class__.__module__.split(".")[0]
712
+
713
+ if type(name) is not str:
714
+ raise TypeError("type(name) must be str")
715
+
716
+ if name in ("format", "format_map") and (
717
+ isinstance(_ob, str) or (isinstance(_ob, type) and issubclass(_ob, str))
718
+ ):
719
+ raise NotImplementedError(
720
+ "Using the format and format_map methods of `str` is not safe"
721
+ )
722
+
723
+ if name in INSPECT_ATTRIBUTES:
724
+ raise AttributeError(f'"{name}" is a restricted name.')
725
+
726
+ # Code defined in the Sandbox namespace can access its own underscore variables
727
+ if name.startswith("_"):
728
+ if self._same_module(module):
729
+ if name.startswith("__") and name not in SAFE_INTERNAL_DUNDER_READ_ATTRIBUTES:
730
+ raise AttributeError(
731
+ f'"{name}" is an invalid attribute name because it starts with "_"'
732
+ )
733
+ else:
734
+ # Nothing can read dunder attributes except those on our safe list
735
+ if name not in SAFE_EXTERNAL_DUNDER_READ_ATTRIBUTES:
736
+ raise AttributeError(
737
+ f'"{name}" is an invalid attribute name because it starts with "__"'
738
+ )
739
+
740
+ exports = getattr(_ob, "__exports__", None)
741
+
742
+ if exports:
743
+ if name not in exports:
744
+ raise AttributeError(f'"{name}" is an invalid attribute name (not in __exports__)')
745
+ elif is_module and (module not in ALLOWED_MODULES or name not in ALLOWED_MODULES[module]):
746
+ raise AttributeError(f'"{name}" is an invalid attribute name (not in ALLOWED_MODULES)')
747
+
748
+ return getattr(_ob, name, default)
749
+
750
+ def _safe_import(
751
+ self,
752
+ name: str,
753
+ globals: Any = None,
754
+ locals: Any = None,
755
+ fromlist: Sequence[str] = (),
756
+ level: int = 0,
757
+ ) -> Any:
758
+ if not self._same_module(name):
759
+ # Disallow importing anything not explicitly allowed by ALLOWED_MODULES
760
+ if name not in ALLOWED_MODULES:
761
+ raise ImportError(f"{name!r} is not an allowed import.")
762
+
763
+ if fromlist is not None:
764
+ for item in fromlist:
765
+ if item not in ALLOWED_MODULES.get(name, set()):
766
+ raise ImportError(f"{item!r} is not an allowed import from {name!r}.")
767
+
768
+ # evaluate the module in the sandbox if needed
399
769
  self._evaluate_module(name)
400
770
 
401
- return __import__(name, *args, **kwargs)
771
+ return __import__(name, globals, locals, fromlist, level)
402
772
 
403
773
  def execute(self) -> dict:
404
774
  """Execute the given code in a restricted sandbox."""
@@ -408,3 +778,15 @@ class Sandbox:
408
778
  exec(self.compile_result.code, self.scope)
409
779
 
410
780
  return self.scope
781
+
782
+
783
+ def sandbox_from_module(base_path: Path, module_name: str) -> Sandbox:
784
+ """Sandbox the code execution."""
785
+ module_path = base_path / str(module_name.replace(".", "/") + ".py")
786
+
787
+ if not module_path.exists():
788
+ raise ModuleNotFoundError(f'Could not load module "{module_name}"')
789
+
790
+ sandbox = Sandbox(module_path, namespace=module_name)
791
+
792
+ return sandbox