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
@@ -1,108 +0,0 @@
1
- import decimal
2
- from datetime import date, datetime
3
- from typing import get_origin
4
-
5
- import pytest
6
- import requests
7
-
8
- import settings
9
- from canvas_sdk.commands.base import _BaseCommand
10
- from canvas_sdk.commands.constants import ClinicalQuantity, Coding
11
- from canvas_sdk.commands.tests.test_utils import (
12
- COMMANDS,
13
- MaskedValue,
14
- create_new_note,
15
- get_token,
16
- )
17
-
18
-
19
- @pytest.fixture(scope="session")
20
- def token() -> MaskedValue:
21
- """Get a valid token."""
22
- return get_token()
23
-
24
-
25
- @pytest.fixture(scope="session")
26
- def new_note(token: MaskedValue) -> dict:
27
- """Create a new note."""
28
- return create_new_note(token)
29
-
30
-
31
- @pytest.fixture
32
- def command_type_map() -> dict[str, type]:
33
- """Map of command field types to their corresponding Python types."""
34
- return {
35
- "AutocompleteField": str,
36
- "MultiLineTextField": str,
37
- "TextField": str,
38
- "ChoiceField": str,
39
- "DateField": datetime,
40
- "ApproximateDateField": date,
41
- "IntegerField": int,
42
- "DecimalField": decimal.Decimal,
43
- }
44
-
45
-
46
- @pytest.mark.integtest
47
- @pytest.mark.parametrize(
48
- "Command",
49
- COMMANDS,
50
- )
51
- def test_command_schema_matches_command_api(
52
- token: MaskedValue,
53
- command_type_map: dict[str, str],
54
- new_note: dict,
55
- Command: _BaseCommand,
56
- ) -> None:
57
- """Test that the command schema matches the command API."""
58
- # first create the command in the new note
59
- data = {"noteKey": new_note["externallyExposableId"], "schemaKey": Command.Meta.key}
60
- headers = {"Authorization": f"Bearer {token.value}"}
61
- url = f"{settings.INTEGRATION_TEST_URL}/core/api/v1/commands/"
62
- command_resp = requests.post(url, headers=headers, data=data).json()
63
- assert "uuid" in command_resp
64
- command_uuid = command_resp["uuid"]
65
-
66
- # next, request the fields of the newly created command
67
- url = f"{settings.INTEGRATION_TEST_URL}/core/api/v1/commands/{command_uuid}/fields/"
68
- command_fields_resp = requests.get(url, headers=headers).json()
69
- assert command_fields_resp["schema"] == Command.Meta.key
70
-
71
- command_fields = command_fields_resp["fields"]
72
- if Command.Meta.key == "questionnaire":
73
- # questionnaire's fields vary per questionnaire, so just check the first two fields which never vary
74
- command_fields = command_fields[:2]
75
- expected_fields = Command.command_schema()
76
- assert len(command_fields) == len(expected_fields)
77
-
78
- for actual_field in command_fields:
79
- name = actual_field["name"]
80
- assert name in expected_fields
81
- expected_field = expected_fields[name]
82
-
83
- assert expected_field["required"] == actual_field["required"]
84
-
85
- expected_type = expected_field["type"]
86
- if expected_type is Coding:
87
- expected_type = expected_type.__annotations__["code"]
88
-
89
- if expected_type is ClinicalQuantity:
90
- expected_type = expected_type.__annotations__["representative_ndc"]
91
-
92
- actual_type = command_type_map.get(actual_field["type"])
93
- if actual_field["type"] == "AutocompleteField" and name[-1] == "s":
94
- # this condition initially created for Prescribe.indications,
95
- # but could apply to other AutocompleteField fields that are lists
96
- # making the assumption here that if the field ends in 's' (like indications), it is a list
97
- assert get_origin(expected_type) is list
98
-
99
- else:
100
- assert expected_type == actual_type
101
-
102
- if (choices := actual_field["choices"]) is None:
103
- assert expected_field["choices"] is None
104
- continue
105
-
106
- assert len(expected_field["choices"]) == len(choices)
107
- for choice in choices:
108
- assert choice["value"] in expected_field["choices"]
@@ -1,81 +0,0 @@
1
- import datetime
2
- import uuid
3
- from enum import Enum
4
-
5
- import pytest
6
-
7
- from canvas_sdk.commands.base import _BaseCommand
8
-
9
-
10
- class DummyEnum(Enum):
11
- """A dummy enum class for testing purposes."""
12
-
13
- LOW = "low"
14
- HIGH = "high"
15
-
16
-
17
- class DummyCommand(_BaseCommand):
18
- """A dummy command class for testing purposes."""
19
-
20
- class Meta:
21
- key = "dummyCommand"
22
-
23
- # Fields
24
- int_field: int = 0
25
- str_field: str = ""
26
- enum_field: DummyEnum | None = None
27
- date_field: datetime.date | None = None
28
- uuid_field: uuid.UUID | None = None
29
-
30
-
31
- @pytest.fixture
32
- def dummy_command_instance() -> DummyCommand:
33
- """Fixture to return a mock instance of DummyCommand for testing."""
34
- cmd = DummyCommand(int_field=10, str_field="hello")
35
- # Set additional fields after instantiation.
36
- cmd.enum_field = DummyEnum.HIGH
37
- cmd.date_field = datetime.date(2025, 2, 14)
38
- cmd.uuid_field = uuid.UUID("12345678-1234-5678-1234-567812345678")
39
- # Set note_uuid and command_uuid for effect methods.
40
- cmd.note_uuid = "note-123"
41
- cmd.command_uuid = "cmd-456"
42
- return cmd
43
-
44
-
45
- def test_dirty_keys(dummy_command_instance: DummyCommand) -> None:
46
- """Test that the dirty_keys property correctly tracks all fields that are set (via constructor and subsequent assignment)."""
47
- keys = set(dummy_command_instance._dirty_keys)
48
- expected_keys = {"int_field", "str_field", "enum_field", "date_field", "uuid_field"}
49
- assert expected_keys == keys
50
-
51
-
52
- def test_values_transformation(dummy_command_instance: DummyCommand) -> None:
53
- """
54
- Test that the values property applies type-specific transformations:
55
- - Enums are replaced by their .value.
56
- - Date/datetime fields are converted to ISO formatted strings.
57
- - UUID fields are converted to strings.
58
- - Other types are returned as-is.
59
- """
60
- vals = dummy_command_instance.values
61
- assert vals["int_field"] == 10
62
- assert vals["str_field"] == "hello"
63
- # For enum_field, should return its .value.
64
- assert vals["enum_field"] == DummyEnum.HIGH.value
65
- # For date_field, should return an ISO string.
66
-
67
- assert (
68
- vals["date_field"] == dummy_command_instance.date_field.isoformat()
69
- if dummy_command_instance.date_field
70
- else None
71
- )
72
- # For uuid_field, should return a string.
73
- assert vals["uuid_field"] == str(dummy_command_instance.uuid_field)
74
-
75
-
76
- def test_constantized_key(dummy_command_instance: DummyCommand) -> None:
77
- """
78
- Test that constantized_key transforms the Meta.key from 'dummyCommand'
79
- into an uppercase, underscore-separated string ('DUMMY_COMMAND').
80
- """
81
- assert dummy_command_instance.constantized_key() == "DUMMY_COMMAND"
@@ -1,375 +0,0 @@
1
- import random
2
- import shutil
3
- import string
4
- import threading
5
- from contextlib import chdir
6
- from datetime import datetime
7
- from decimal import Decimal
8
- from pathlib import Path
9
- from typing import Any, cast
10
- from urllib.parse import urlparse
11
-
12
- import pytest
13
- import requests
14
- import websocket
15
- from pydantic import ValidationError
16
- from typer.testing import CliRunner
17
-
18
- import settings
19
- from canvas_cli.apps.plugin.plugin import _build_package, plugin_url
20
- from canvas_cli.main import app
21
- from canvas_sdk.commands import (
22
- AssessCommand,
23
- DiagnoseCommand,
24
- GoalCommand,
25
- HistoryOfPresentIllnessCommand,
26
- MedicationStatementCommand,
27
- PlanCommand,
28
- PrescribeCommand,
29
- QuestionnaireCommand,
30
- ReasonForVisitCommand,
31
- StopMedicationCommand,
32
- UpdateGoalCommand,
33
- )
34
- from canvas_sdk.commands.base import _BaseCommand
35
- from canvas_sdk.commands.constants import ClinicalQuantity, Coding
36
-
37
- runner = CliRunner()
38
-
39
-
40
- class WrongType:
41
- """A type to yield ValidationErrors in tests."""
42
-
43
- wrong_field: str
44
-
45
-
46
- class MaskedValue:
47
- """A class to mask sensitive values in tests."""
48
-
49
- def __init__(self, value: str) -> None:
50
- self.value = value
51
-
52
- def __repr__(self) -> str:
53
- return "MaskedValue(********)"
54
-
55
- def __str___(self) -> str:
56
- return "*******"
57
-
58
-
59
- def get_field_type_unformatted(field_props: dict[str, Any]) -> str:
60
- """Get the unformatted field type from the field properties."""
61
- if t := field_props.get("type"):
62
- return field_props.get("format") or t
63
-
64
- first_in_union: dict = field_props.get("anyOf", field_props.get("allOf"))[0]
65
- if "$ref" in first_in_union:
66
- return first_in_union["$ref"].split("#/$defs/")[-1]
67
- return first_in_union.get("format") or first_in_union["type"]
68
-
69
-
70
- def get_field_type(field_props: dict) -> str:
71
- """Get the field type from the field properties."""
72
- return get_field_type_unformatted(field_props).replace("-", "").replace("array", "list")
73
-
74
-
75
- def random_string() -> str:
76
- """Generate a random string."""
77
- return "".join(random.choices(string.ascii_uppercase + string.digits, k=7))
78
-
79
-
80
- def fake(field_props: dict, Command: type[_BaseCommand]) -> Any:
81
- """Generate a fake value for a field."""
82
- t = get_field_type(field_props)
83
- match t:
84
- case "string":
85
- return random_string()
86
- case "integer":
87
- return random.randint(1, 10)
88
- case "datetime":
89
- return datetime.now()
90
- case "boolean":
91
- return random.choice([True, False])
92
- case "number":
93
- return Decimal(random.randrange(1, 200))
94
- case "array":
95
- num_items = random.randint(0, 5)
96
- item_props = field_props["anyOf"][0]["items"]
97
- return [fake(item_props, Command) for i in range(num_items)]
98
- case "list":
99
- num_items = random.randint(0, field_props.get("maxItems", 5))
100
- item_props = field_props.get("items")
101
- return [fake(item_props, Command) for i in range(num_items)] if item_props else []
102
- case "Coding":
103
- return Coding(system=random_string(), code=random_string(), display=random_string())
104
- case "ClinicalQuantity":
105
- return ClinicalQuantity(representative_ndc="ndc", ncpdp_quantity_qualifier_code="code")
106
- case "WrongType":
107
- return WrongType()
108
- if t[0].isupper():
109
- return random.choice(list(getattr(Command, t)))
110
-
111
-
112
- def raises_wrong_type_error(
113
- Command: type[_BaseCommand],
114
- field: str,
115
- ) -> None:
116
- """Test that the correct error is raised when the wrong type is passed to a field."""
117
- field_props = Command.model_json_schema()["properties"][field]
118
- field_type = get_field_type(field_props)
119
-
120
- wrong_field_type = "WrongType"
121
-
122
- with pytest.raises(ValidationError) as e1:
123
- err_kwargs = {field: fake({"type": wrong_field_type}, Command)}
124
- Command(**err_kwargs)
125
- err_msg1 = repr(e1.value)
126
-
127
- valid_kwargs = {field: fake(field_props, Command)}
128
- cmd = Command(**valid_kwargs)
129
- err_value = fake({"type": wrong_field_type}, Command)
130
- with pytest.raises(ValidationError) as e2:
131
- setattr(cmd, field, err_value)
132
- err_msg2 = repr(e2.value)
133
-
134
- assert "validation error" in err_msg1
135
- assert f"{Command.__name__}\n{field}" in err_msg1
136
-
137
- assert "validation error" in err_msg2
138
- assert f"{Command.__name__}\n{field}" in err_msg1
139
-
140
- field_type = (
141
- "dictionary" if field_type == "Coding" or field_type == "ClinicalQuantity" else field_type
142
- )
143
- if field_type == "number":
144
- assert "Input should be an instance of Decimal" in err_msg1
145
- assert "Input should be an instance of Decimal" in err_msg2
146
- elif field_type[0].isupper():
147
- assert f"Input should be an instance of {Command.__name__}.{field_type}" in err_msg1
148
- assert f"Input should be an instance of {Command.__name__}.{field_type}" in err_msg2
149
- else:
150
- assert f"Input should be a valid {field_type}" in err_msg1
151
- assert f"Input should be a valid {field_type}" in err_msg2
152
-
153
-
154
- def raises_none_error_for_effect_method(
155
- Command: type[_BaseCommand],
156
- method: str,
157
- ) -> None:
158
- """Test that the correct error is raised when a required field is None for an effect method."""
159
- cmd_name = Command.__name__
160
- cmd_name_article = "an" if cmd_name.startswith(("A", "E", "I", "O", "U")) else "a"
161
-
162
- cmd = Command()
163
- method_required_fields = cmd._get_effect_method_required_fields(method)
164
- with pytest.raises(ValidationError) as e:
165
- getattr(cmd, method)()
166
- e_msg = repr(e.value)
167
- missing_fields = [field for field in method_required_fields if getattr(cmd, field) is None]
168
- num_errs = len(missing_fields)
169
- assert f"{num_errs} validation error{'s' if num_errs > 1 else ''} for {cmd_name}" in e_msg
170
-
171
- for f in missing_fields:
172
- assert (
173
- f"Field '{f}' is required to {method.replace('_', ' ')} {cmd_name_article} {cmd_name} [type=missing, input_value=None, input_type=NoneType]"
174
- in e_msg
175
- )
176
-
177
-
178
- def write_protocol_code(
179
- note_uuid: str, plugin_name: str, commands: list[type[_BaseCommand]]
180
- ) -> None:
181
- """Test that the protocol code is written correctly."""
182
- imports = ", ".join([c.__name__ for c in commands])
183
- effects = ", ".join([f"{c.__name__}(note_uuid='{note_uuid}').originate()" for c in commands])
184
-
185
- protocol_code = f"""from canvas_sdk.commands import {imports}
186
- from canvas_sdk.events import EventType
187
- from canvas_sdk.protocols import BaseProtocol
188
-
189
- class Protocol(BaseProtocol):
190
- RESPONDS_TO = EventType.Name(EventType.ENCOUNTER_CREATED)
191
- def compute(self):
192
- return [{effects}]
193
- """
194
-
195
- with chdir(Path("./custom-plugins")):
196
- runner.invoke(app, "init", input=plugin_name)
197
-
198
- with open(f"./custom-plugins/{plugin_name}/protocols/my_protocol.py", "w") as protocol:
199
- protocol.write(protocol_code)
200
-
201
-
202
- def install_plugin(plugin_name: str, token: MaskedValue) -> None:
203
- """Install a plugin."""
204
- with open(_build_package(Path(f"./custom-plugins/{plugin_name}")), "rb") as package:
205
- response = requests.post(
206
- plugin_url(cast(str, settings.INTEGRATION_TEST_URL)),
207
- data={"is_enabled": True},
208
- files={"package": package},
209
- headers={"Authorization": f"Bearer {token.value}"},
210
- )
211
- response.raise_for_status()
212
-
213
-
214
- def trigger_plugin_event(token: MaskedValue) -> None:
215
- """Trigger a plugin event."""
216
- response = requests.post(
217
- f"{settings.INTEGRATION_TEST_URL}/api/Note/",
218
- headers={
219
- "Authorization": f"Bearer {token.value}",
220
- "Content-Type": "application/json",
221
- "Accept": "application/json",
222
- },
223
- json={
224
- "patient": 2,
225
- "provider": 1,
226
- "note_type": "office",
227
- "note_type_version": 1,
228
- "lastModifiedBySessionKey": "8fee3c03a525cebee1d8a6b8e63dd4dg",
229
- },
230
- )
231
- response.raise_for_status()
232
-
233
-
234
- def get_original_note_body_commands(new_note_id: int, token: MaskedValue) -> list[str]:
235
- """Get the commands from the original note body."""
236
- response = requests.get(
237
- f"{settings.INTEGRATION_TEST_URL}/api/Note/{new_note_id}",
238
- headers={
239
- "Authorization": f"Bearer {token.value}",
240
- "Content-Type": "application/json",
241
- "Accept": "application/json",
242
- },
243
- )
244
- response.raise_for_status()
245
-
246
- original_note = response.json()
247
-
248
- body = original_note["body"]
249
- return [
250
- line["value"]
251
- for line in body
252
- if "data" in line
253
- and "commandUuid" in line["data"]
254
- and "id" in line["data"]
255
- and line["type"] == "command"
256
- ]
257
-
258
-
259
- def clean_up_files_and_plugins(plugin_name: str, token: MaskedValue) -> None:
260
- """Clean up the files and plugins."""
261
- # clean up
262
- if Path(f"./custom-plugins/{plugin_name}").exists():
263
- shutil.rmtree(Path(f"./custom-plugins/{plugin_name}"))
264
-
265
- # disable
266
- response = requests.patch(
267
- plugin_url(cast(str, settings.INTEGRATION_TEST_URL), plugin_name),
268
- data={"is_enabled": False},
269
- headers={
270
- "Authorization": f"Bearer {token.value}",
271
- },
272
- )
273
- response.raise_for_status()
274
-
275
- # delete
276
- response = requests.delete(
277
- plugin_url(cast(str, settings.INTEGRATION_TEST_URL), plugin_name),
278
- headers={"Authorization": f"Bearer {token.value}"},
279
- )
280
- response.raise_for_status()
281
-
282
-
283
- # For reuse with the protocol code
284
- COMMANDS: list[type[_BaseCommand]] = [
285
- AssessCommand,
286
- DiagnoseCommand,
287
- GoalCommand,
288
- HistoryOfPresentIllnessCommand,
289
- MedicationStatementCommand,
290
- PlanCommand,
291
- PrescribeCommand,
292
- QuestionnaireCommand,
293
- ReasonForVisitCommand,
294
- StopMedicationCommand,
295
- UpdateGoalCommand,
296
- ]
297
-
298
-
299
- def create_new_note(token: MaskedValue) -> dict:
300
- """Create a new note."""
301
- headers = {
302
- "Authorization": f"Bearer {token.value}",
303
- "Content-Type": "application/json",
304
- "Accept": "application/json",
305
- }
306
- data = {
307
- "patient": 1,
308
- "provider": 1,
309
- "note_type": "office",
310
- "note_type_version": 1,
311
- "lastModifiedBySessionKey": "8fee3c03a525cebee1d8a6b8e63dd4dg",
312
- }
313
- response = requests.post(
314
- f"{settings.INTEGRATION_TEST_URL}/api/Note/", headers=headers, json=data
315
- )
316
- response.raise_for_status()
317
- return response.json()
318
-
319
-
320
- def get_token() -> MaskedValue:
321
- """Get a valid token."""
322
- response = requests.post(
323
- f"{settings.INTEGRATION_TEST_URL}/auth/token/",
324
- headers={"Content-Type": "application/x-www-form-urlencoded"},
325
- data={
326
- "grant_type": "client_credentials",
327
- "client_id": settings.INTEGRATION_TEST_CLIENT_ID,
328
- "client_secret": settings.INTEGRATION_TEST_CLIENT_SECRET,
329
- },
330
- )
331
- response.raise_for_status()
332
-
333
- return MaskedValue(response.json()["access_token"])
334
-
335
-
336
- def wait_for_log(
337
- host: str, token: str, message: str
338
- ) -> tuple[threading.Event, threading.Thread, websocket.WebSocketApp]:
339
- """Wait for a specific log message."""
340
- hostname = cast(str, urlparse(host).hostname)
341
- instance = hostname.removesuffix(".canvasmedical.com")
342
-
343
- websocket_uri = f"wss://logs.console.canvasmedical.com/{instance}?token={token}"
344
-
345
- connected_event = threading.Event()
346
- message_received_event = threading.Event()
347
-
348
- def _on_message(ws: websocket.WebSocket, received_message: str) -> None:
349
- try:
350
- if "Log stream connected" in received_message:
351
- connected_event.set()
352
- if message.lower() in received_message.lower():
353
- message_received_event.set()
354
- ws.close()
355
- except Exception as ex:
356
- print(f"Error processing message: {ex}")
357
-
358
- def _on_error(ws: websocket.WebSocket, error: str) -> None:
359
- print(f"WebSocket error: {error}")
360
-
361
- ws = websocket.WebSocketApp(
362
- websocket_uri,
363
- on_message=_on_message,
364
- on_error=_on_error,
365
- )
366
-
367
- thread = threading.Thread(target=ws.run_forever)
368
- thread.start()
369
-
370
- timeout_not_hit = connected_event.wait(timeout=5.0)
371
- if not timeout_not_hit:
372
- ws.close()
373
- assert timeout_not_hit, "connection timeout hit"
374
-
375
- return message_received_event, thread, ws
File without changes