canvas 0.18.0__py3-none-any.whl → 0.19.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 (29) hide show
  1. {canvas-0.18.0.dist-info → canvas-0.19.1.dist-info}/METADATA +1 -1
  2. {canvas-0.18.0.dist-info → canvas-0.19.1.dist-info}/RECORD +28 -18
  3. canvas_cli/utils/validators/manifest_schema.py +12 -0
  4. canvas_sdk/__init__.py +4 -2
  5. canvas_sdk/commands/commands/reason_for_visit.py +18 -3
  6. canvas_sdk/commands/tests/test_utils.py +15 -3
  7. canvas_sdk/commands/tests/unit/tests.py +7 -16
  8. canvas_sdk/questionnaires/__init__.py +3 -0
  9. canvas_sdk/questionnaires/tests/__init__.py +0 -0
  10. canvas_sdk/questionnaires/tests/test_utils.py +74 -0
  11. canvas_sdk/questionnaires/utils.py +117 -0
  12. canvas_sdk/templates/utils.py +7 -12
  13. canvas_sdk/utils/plugins.py +25 -0
  14. canvas_sdk/v1/data/__init__.py +4 -1
  15. canvas_sdk/v1/data/billing.py +29 -2
  16. canvas_sdk/v1/data/coverage.py +1 -1
  17. canvas_sdk/v1/data/reason_for_visit.py +22 -0
  18. plugin_runner/plugin_installer.py +23 -32
  19. plugin_runner/sandbox.py +6 -6
  20. plugin_runner/tests/fixtures/plugins/test_load_questionnaire/CANVAS_MANIFEST.json +52 -0
  21. plugin_runner/tests/fixtures/plugins/test_load_questionnaire/README.md +11 -0
  22. plugin_runner/tests/fixtures/plugins/test_load_questionnaire/protocols/__init__.py +0 -0
  23. plugin_runner/tests/fixtures/plugins/test_load_questionnaire/protocols/my_protocol.py +39 -0
  24. plugin_runner/tests/fixtures/plugins/test_load_questionnaire/questionnaires/example_questionnaire.yml +61 -0
  25. plugin_runner/tests/test_plugin_runner.py +12 -12
  26. settings.py +22 -12
  27. canvas_sdk/utils/db.py +0 -17
  28. {canvas-0.18.0.dist-info → canvas-0.19.1.dist-info}/WHEEL +0 -0
  29. {canvas-0.18.0.dist-info → canvas-0.19.1.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: canvas
3
- Version: 0.18.0
3
+ Version: 0.19.1
4
4
  Summary: SDK to customize event-driven actions in your Canvas instance
5
5
  Author-email: Canvas Team <engineering@canvasmedical.com>
6
6
  License-Expression: MIT
@@ -1,4 +1,4 @@
1
- settings.py,sha256=wtQMRHsbynzEjZb5WmUMV3q27oWc6prLS9ztnEAsqMg,2829
1
+ settings.py,sha256=Mk332hOSmNyLVRKLUhtc1Kll-P5Iun7vbkZ0chwnOAs,3176
2
2
  canvas_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  canvas_cli/conftest.py,sha256=0mEjjU7_w9nq-HCvscxFJClw-EeyQYujNLLRPssksZQ,931
4
4
  canvas_cli/main.py,sha256=L6JQkt1yxy30cA3-M9v7JD8WMW4i0M5GPr9kZetAito,2728
@@ -95,7 +95,7 @@ canvas_cli/utils/urls/__init__.py,sha256=08hlrQhQ1pKBjlIRaC0j53IkgK723jfK8-j3djv
95
95
  canvas_cli/utils/urls/tests.py,sha256=opXDF2i3lXTdsKJ7ywIRzWDDzZ5KAO0JbGIR3hbJdoE,407
96
96
  canvas_cli/utils/urls/urls.py,sha256=KwWTh5ERrEsZEvdBrZpZB71xtyWkDuglpXUbycWmBOo,798
97
97
  canvas_cli/utils/validators/__init__.py,sha256=rBvSR2O1hWkNAnUBdcr-zUkmqT796_A61b9pnvEhwrM,113
98
- canvas_cli/utils/validators/manifest_schema.py,sha256=BDOi4wj3AZcKyV6ItNrDwIPun_7ozK3Yacc6hDTtLFg,4171
98
+ canvas_cli/utils/validators/manifest_schema.py,sha256=Bp3FFdIY7KFz1AXnQAbJa6_E1pm9PCc0CXGkwK4JdZo,4573
99
99
  canvas_cli/utils/validators/tests.py,sha256=KhaOdbxGUK2QwL2KnycAJJkqclYQSXF7CKg-scSf0DU,1259
100
100
  canvas_cli/utils/validators/validators.py,sha256=lrUBQ0sF7seTe_pNGsNgASdr2BGp6g-Qui58V4H9qfQ,1598
101
101
  canvas_generated/messages/effects_pb2.py,sha256=qGIcJpjSrhLvj6Km2wMRKRPJ2IxymA0M_R9SghvilII,9821
@@ -110,7 +110,7 @@ canvas_generated/messages/plugins_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8gXHDCN
110
110
  canvas_generated/services/plugin_runner_pb2.py,sha256=RfAo_imYoSuoexq-1IHhMhXZgQpzq91pqugxsigL8NU,1557
111
111
  canvas_generated/services/plugin_runner_pb2.pyi,sha256=1w-Pa4k7HtlmQAr7B6sgV64zdZplBKQKHN-S8bjwO3w,265
112
112
  canvas_generated/services/plugin_runner_pb2_grpc.py,sha256=EzJJVkP_AZ3dwBA7OxUito0NSalRmjjg8q9TZ_P18ww,4549
113
- canvas_sdk/__init__.py,sha256=d7V1Qsp4hSqp8opmvqp-0J33uibArUjwENMfzSDAdZg,102
113
+ canvas_sdk/__init__.py,sha256=7IuwjhqIaOO5BgCfXi-B1eV6XfK9Ku5PHPWuTGeDNgU,151
114
114
  canvas_sdk/base.py,sha256=aaWilIx70lsAE76x7QaJolyIvcFnXDGPw1UMv9t0FDw,2035
115
115
  canvas_sdk/commands/__init__.py,sha256=cNQy1Y_Ji3cPush2hbaoXC3pbIFmobv_Vm-UG0w0hlc,2696
116
116
  canvas_sdk/commands/base.py,sha256=ScfOo4QXTInhTf6OGLT5dEu81yhZAcgVT8qxwIKWbow,5155
@@ -132,7 +132,7 @@ canvas_sdk/commands/commands/perform.py,sha256=FpumHzPJp5pzb4F0CBW2QbSWdR_5mpib1
132
132
  canvas_sdk/commands/commands/plan.py,sha256=uxjXmdNcG32R4S0CJ7fqDaaXTMiJJH9RuvwlWuztlZ0,403
133
133
  canvas_sdk/commands/commands/prescribe.py,sha256=CWk25bQtpHb7KTNnCIPGaIGEnUQagAJ8fnbRnyPIssc,2176
134
134
  canvas_sdk/commands/commands/questionnaire.py,sha256=Giu4stTzpddO8fH3iVyxm2lUeDwe7AmhfuIfb_0WUj4,644
135
- canvas_sdk/commands/commands/reason_for_visit.py,sha256=NX_hKyWg2HXFO03uWLOFwrO1v2-kVeez1CSlFIQ38eA,1479
135
+ canvas_sdk/commands/commands/reason_for_visit.py,sha256=XapED7uU9yIUPkJ5p0PUboBNX7MlpM9PYfmOmAkAdpY,2242
136
136
  canvas_sdk/commands/commands/refill.py,sha256=HC9Nl3_BkGn3HRZ-4YNT9lqIywFVom6PyJChK3_0Zyc,458
137
137
  canvas_sdk/commands/commands/remove_allergy.py,sha256=6CXo3s63z7oqMlROExPWudQBZeSvD3x0SC_efQSb0Lw,748
138
138
  canvas_sdk/commands/commands/review_of_systems.py,sha256=F2wXmeh5yEuWokC5I2_H-9k32PgYBUcfyLHgYVL4LPQ,276
@@ -142,13 +142,13 @@ canvas_sdk/commands/commands/task.py,sha256=p-WskHqj_q8TqllarEw73AIW6n2J7uzyIso9
142
142
  canvas_sdk/commands/commands/update_diagnosis.py,sha256=fqPcKBaiRemo8Qa4H4g6ZvKM2YczVlj9FxpBGjs8J9Q,824
143
143
  canvas_sdk/commands/commands/update_goal.py,sha256=ddvxlDnoTm5k-G5aJ-pW2fMIC-s5K8jYia9tISXsxAk,1580
144
144
  canvas_sdk/commands/commands/vitals.py,sha256=hOuDXbNwqJbsBAvvkpjrXWqssrdQPxEfCox1NlI0h4A,3127
145
- canvas_sdk/commands/tests/test_utils.py,sha256=qkzcTDjyvrpXyPkm3f6KBVy-4lqJ1cOq2QSc-bPVPsU,12004
145
+ canvas_sdk/commands/tests/test_utils.py,sha256=Jrkbz2xltrjUVYRdIyNaKdq5tCtGQ4vygic7hpBp6Ek,12157
146
146
  canvas_sdk/commands/tests/protocol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
147
147
  canvas_sdk/commands/tests/protocol/tests.py,sha256=e0fiJUKsfi0aYSlaTdrMHybZntC7oBdaHks9PSrMuhU,2168
148
148
  canvas_sdk/commands/tests/schema/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
149
149
  canvas_sdk/commands/tests/schema/tests.py,sha256=rSu146RouMrPLv0G-E6QtgQhG2vkUCCk1M3Baa6fXBg,3701
150
150
  canvas_sdk/commands/tests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
151
- canvas_sdk/commands/tests/unit/tests.py,sha256=FsjNpUxkS4F89g341uuc5GkMCTp_JACTx32wxcwYcWg,9948
151
+ canvas_sdk/commands/tests/unit/tests.py,sha256=uzQbGIgAM1PTCGJy0p8etHx0zhOB0GDVWO7WCaxZuUs,8767
152
152
  canvas_sdk/effects/__init__.py,sha256=rcMI7IaVe8kDaYRngvlH5JPOL7FBSGIoLZB4UCN18d8,151
153
153
  canvas_sdk/effects/base.py,sha256=znIaDNkCYbzJxf4Q80hLRYUG5H73c8IMq1UCzeBcXjw,714
154
154
  canvas_sdk/effects/launch_modal.py,sha256=2yVY7r6hY5YYiBKae0rpS4Q0UgJPJtGlLuZGj68hm8s,1058
@@ -180,30 +180,34 @@ canvas_sdk/protocols/__init__.py,sha256=3u9zet5D4DX4V953tLCoN1xhaOhAUCwGwscMv-7I
180
180
  canvas_sdk/protocols/base.py,sha256=sbm0uOk3PPfPemqBmHh2hawE5utC6no46EmvyMN8Y7Q,179
181
181
  canvas_sdk/protocols/clinical_quality_measure.py,sha256=8cU93ah9YsPecpZR1-csAbg69oFn9a8LtjHjYMMHedw,4844
182
182
  canvas_sdk/protocols/timeframe.py,sha256=SlTDhTy0TqPXKS9JZFeTVApQJDf8C-NIRLqFJltB17g,1148
183
+ canvas_sdk/questionnaires/__init__.py,sha256=WqEj8iLUjvubIzHVAXUEYQwzUFxgauAX9rPdVvJjbRQ,96
184
+ canvas_sdk/questionnaires/utils.py,sha256=tGTlEYWg6HJkf0zSWXgaSRsh4BW2z6dWNTTPGBU0i5Q,3569
185
+ canvas_sdk/questionnaires/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
186
+ canvas_sdk/questionnaires/tests/test_utils.py,sha256=4NCYPFSmn96wSfEB48eFfM4548sOULAX58Ln49FugEw,3032
183
187
  canvas_sdk/templates/__init__.py,sha256=crz8FE6yoCgwTrqosLDHM7cOiVdhWgWz0l0J--1sgmM,69
184
- canvas_sdk/templates/utils.py,sha256=pjU9ZOAb4MHb8ui4l3f1yWRczy62JCF51rn6fWAjMKY,1645
188
+ canvas_sdk/templates/utils.py,sha256=h-BAX4rm-EpqKMnFGMZF1RbXaWsOr6-ECQYhmJ8skyI,1422
185
189
  canvas_sdk/templates/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
186
190
  canvas_sdk/templates/tests/test_utils.py,sha256=VRahmmVwXKcp1NMLoA3BZL4cFFXzFnD-i5IUpcEeXTg,1832
187
191
  canvas_sdk/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
188
192
  canvas_sdk/utils/__init__.py,sha256=nZEfYeU-qNZBOh39b8zAuEh0Wzh2PgXxs0NRKKe66Pg,184
189
- canvas_sdk/utils/db.py,sha256=0AO5bhu-k9OsAHpXe1RHzyDZnBGHUEzrv8-vYtTIoeA,592
190
193
  canvas_sdk/utils/http.py,sha256=McFtcrgdR2W8XguTaTF54O5Xf5r5buoYDldDM5H5ahY,5846
194
+ canvas_sdk/utils/plugins.py,sha256=853MW2fiLpyG3o9ISEawAthQeRiZP73cai5Tngwu4MY,767
191
195
  canvas_sdk/utils/stats.py,sha256=sJhIW_IssUVefQN6rrUAt1P0KvVIUIYcnpZlMHLibNA,732
192
196
  canvas_sdk/utils/tests.py,sha256=0Buh_7PvDU1D081_rSJoYSJwIHMOBbL0gtGS3bSKe7s,2285
193
197
  canvas_sdk/v1/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
194
198
  canvas_sdk/v1/apps.py,sha256=z5HVdICPLYrbWM8ZXK89Xu7RWaXuKU4AFRrPAZGz0C4,151
195
199
  canvas_sdk/v1/models.py,sha256=q9Sofiu9JDH5g04H8NHYIqAtBYxH4KjnwOldoKsY9Lk,206
196
- canvas_sdk/v1/data/__init__.py,sha256=1xQqNKh9rSwyLqT8pE8PYNOx3DTxcS9qXuU79OqjEDU,2903
200
+ canvas_sdk/v1/data/__init__.py,sha256=SAAiRSaYly47oB8uyfjf6e4chTULX7M6qisR-fk2eHQ,3052
197
201
  canvas_sdk/v1/data/allergy_intolerance.py,sha256=cm29fCOFiHHHVyff9kreWFUPZM--Nydw3a0FSbWh5-w,2303
198
202
  canvas_sdk/v1/data/appointment.py,sha256=rK4Vl2bE_aE73vGx8Elj-tJdDdIbQNTbl0uLtWme89Y,1900
199
203
  canvas_sdk/v1/data/assessment.py,sha256=MYUrE6xtOVYSRiMuQKMdHuWFvuGx62w4SZJpyFskQ2U,1485
200
204
  canvas_sdk/v1/data/base.py,sha256=_UnCf0SP_HC9FYEzB1BsJ3QcKqXNdCR90Dtx1Gggcf4,6328
201
- canvas_sdk/v1/data/billing.py,sha256=Fk2_hIh6-HeuStxL3t4aZvIuFYdF7Kyn9aKcin0ypOU,1960
205
+ canvas_sdk/v1/data/billing.py,sha256=dPQb9vj2mIhlU5cSfmfTkXw0NRDMmueJojyRNrgx1no,2586
202
206
  canvas_sdk/v1/data/care_team.py,sha256=qy9x-FZ6LI_GrXnohS5izc6e-SANhV50eh_bilaVI4Y,1818
203
207
  canvas_sdk/v1/data/command.py,sha256=r1hFQDFkGTAcpfRqCsQICcKivtSt08Mdz2jcKS8m9TA,1613
204
208
  canvas_sdk/v1/data/common.py,sha256=JB8Lf8pHTo8VttZTnQFthnKLjalX8iI-ZoBsjpVMxHs,4559
205
209
  canvas_sdk/v1/data/condition.py,sha256=olHJFb7A9ORWlMzolpt15fQmg47F--5oEM_lrAQGeMk,2096
206
- canvas_sdk/v1/data/coverage.py,sha256=xFCIwpoiQ7SLzAUgye408XoutAQdYhixpd9MdPt_BhI,10835
210
+ canvas_sdk/v1/data/coverage.py,sha256=buz0VwJvyKJcX-EallQweA-5P0UxSjt6QLg3f8sMHEQ,10836
207
211
  canvas_sdk/v1/data/detected_issue.py,sha256=hvzUIjLx2unA_GNdW1oJcsLRx8jIEOKHWP8j_p88s4k,1685
208
212
  canvas_sdk/v1/data/device.py,sha256=RZ200OCaBRGwdERvx9eFOPTHg56r62Hqp9uJeS2giOo,1613
209
213
  canvas_sdk/v1/data/imaging.py,sha256=2caHmSQmt1zUx5q_AoydkuUqtkQo-tdMk5B_0suSlWw,4297
@@ -216,6 +220,7 @@ canvas_sdk/v1/data/patient.py,sha256=tKsZRaexpanDtCHTy4HtEsodiw7BKk8hPcEwpF98o-g
216
220
  canvas_sdk/v1/data/practicelocation.py,sha256=6W2NzwiK0xXlzXfDom_Wm3ScERd8W6mw8obSTFZWYLc,4206
217
221
  canvas_sdk/v1/data/protocol_override.py,sha256=o-GK7-HkOsyYDLxUyn9BwlZM-13QNuGGNrey5EF44QI,2105
218
222
  canvas_sdk/v1/data/questionnaire.py,sha256=1Vo5QUsOLYbZiQ64QFpgY7Mj6wY8vsV8f9uPiFl0wac,7031
223
+ canvas_sdk/v1/data/reason_for_visit.py,sha256=9rCmvbJZ4dKUUDzqFX1uxlMa_qO57KcRntV3re8q_8g,608
219
224
  canvas_sdk/v1/data/staff.py,sha256=QyFxvFAvNwgTm4gf-uPH6A8ibFE2TBhEckZpAM7gNpw,2762
220
225
  canvas_sdk/v1/data/task.py,sha256=5KNn88APPNOHEk4s1ZJRBBav8-AEQTUH039Vio_ZtAk,3471
221
226
  canvas_sdk/v1/data/team.py,sha256=FlkZsHSSHKtvmlB1XuGPUmC7wl8iMbOPi_fl9sYw-aU,2801
@@ -249,13 +254,13 @@ plugin_runner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
249
254
  plugin_runner/authentication.py,sha256=SDPso2AogtLAV_H0LuMDp99IMZuF3oTq-Q_AXAvJ8uc,1116
250
255
  plugin_runner/aws_headers.py,sha256=DenX_nAMVhXMJZw88PLZbqJsi5_XriNtr3jE-eJqHY4,2773
251
256
  plugin_runner/exceptions.py,sha256=YnRZiQVzbU3HrVlmEXLje_np99009YnhTRVHHyBCtqc,433
252
- plugin_runner/plugin_installer.py,sha256=0gHMpI4CDkWljbU7HLhbPjl2E_oHxDZMlmPtTqCyS9o,7748
257
+ plugin_runner/plugin_installer.py,sha256=8hWllx7YciatEozFRwcp5Y_L_KyEi2e8CGienRWrZs0,7375
253
258
  plugin_runner/plugin_runner.py,sha256=h_FHStJrvRiG9DSf8uXHKFb4aby2wBzLAe10Ov4xnas,16437
254
- plugin_runner/sandbox.py,sha256=SdTfPWzs5mcMmQ5J9ESpTk5c3e1nQG8F4N0pKPuK48M,14499
259
+ plugin_runner/sandbox.py,sha256=P64SKpKZdoJwk2NsirGRKq7xjygBtvdipIAVSD5roY8,14509
255
260
  plugin_runner/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
256
261
  plugin_runner/tests/test_application.py,sha256=e1R2YagMRD96gZALx-Zra-e-sR3SiP7cIpI6pheZnUc,2427
257
262
  plugin_runner/tests/test_plugin_installer.py,sha256=i3ELG5cRfaG7ETLwk8aJhahgJuPfKjIrXL250t2wpzI,4434
258
- plugin_runner/tests/test_plugin_runner.py,sha256=D2NyTGogu1FpiFo0MKZXcJRWFh4Ix6uznYnthdXXzk4,11632
263
+ plugin_runner/tests/test_plugin_runner.py,sha256=i1GAX8VsSU5KMwDOPYOQ5S9nxkRCWSjzG1nnh_IJLu4,11638
259
264
  plugin_runner/tests/test_sandbox.py,sha256=fDIkRBzoVWClvIMOI37FvJ1TRZ5mAztt88YZ0OPqsA8,4231
260
265
  plugin_runner/tests/data/plugins/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
261
266
  plugin_runner/tests/fixtures/plugins/example_plugin/CANVAS_MANIFEST.json,sha256=J9T_E5vqUX4HITHbFsd6JQpw3YvMS4wR_lhI5JL2KMk,749
@@ -271,6 +276,11 @@ plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/templates/__in
271
276
  plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/templates/base.py,sha256=apbdzjKtqeQJpPnAxgKituh0ZG71jK0SlwtQRSiV-4o,170
272
277
  plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/utils/__init__.py,sha256=ZvCN37hADp2VG1-DfJE_dtHrXf-yirF0321f92WzC20,79
273
278
  plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/utils/base.py,sha256=--erTS2NGGcw4aVhabrKTtHtuR5OHjc8rsK30fvuYZs,83
279
+ plugin_runner/tests/fixtures/plugins/test_load_questionnaire/CANVAS_MANIFEST.json,sha256=orbmUtS3uGl6XNiubCo79H4Jr_DbydMtzvdXUuk6cAM,1579
280
+ plugin_runner/tests/fixtures/plugins/test_load_questionnaire/README.md,sha256=D3SFO-pnyZjpEFLylWaIWi8AmUW8ZxMyHDddN4a7vAQ,246
281
+ plugin_runner/tests/fixtures/plugins/test_load_questionnaire/protocols/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
282
+ plugin_runner/tests/fixtures/plugins/test_load_questionnaire/protocols/my_protocol.py,sha256=_dWsMARIefDi8FlMhZIN0j62-0EjXimdfu42ZEhtDzU,1444
283
+ plugin_runner/tests/fixtures/plugins/test_load_questionnaire/questionnaires/example_questionnaire.yml,sha256=PjbCH4bbRFDmrciM-mJWjrKG7KTFhcSgZu1w6dAorio,1940
274
284
  plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/CANVAS_MANIFEST.json,sha256=o84LSegKZ7eH_MeGkmRZFJJPEcOYHUzT0P2QkQpCZac,793
275
285
  plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/README.md,sha256=ZuFqRirQNoqdPzBjfpYzTGpIogmiQ_noX59hY0W2KHU,292
276
286
  plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/other_module/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -318,7 +328,7 @@ protobufs/canvas_generated/messages/plugins.proto,sha256=oNainUPWFYQjgCX7bJEPI9_
318
328
  protobufs/canvas_generated/services/plugin_runner.proto,sha256=doadBKn5k4xAtOgR-q_pEvW4yzxpUaHNOowMG6CL5GY,304
319
329
  pubsub/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
320
330
  pubsub/pubsub.py,sha256=pyTW0JU8mtaqiAV6g6xjZwel1CVy2EonPMU-_vkmhUM,1044
321
- canvas-0.18.0.dist-info/METADATA,sha256=qAXIh8PHMwAY2D85FKRWX0i-6x5B0_AA9BRKzOLGr2g,4375
322
- canvas-0.18.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
323
- canvas-0.18.0.dist-info/entry_points.txt,sha256=0Vs_9GmTVUNniH6eDBlRPgofmADMV4BES6Ao26M4AbM,47
324
- canvas-0.18.0.dist-info/RECORD,,
331
+ canvas-0.19.1.dist-info/METADATA,sha256=7w1TpwcKJ-BjC9HRzHYW4OW6LzUFr1gHFxlqRrIkX-U,4375
332
+ canvas-0.19.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
333
+ canvas-0.19.1.dist-info/entry_points.txt,sha256=0Vs_9GmTVUNniH6eDBlRPgofmADMV4BES6Ao26M4AbM,47
334
+ canvas-0.19.1.dist-info/RECORD,,
@@ -16,6 +16,7 @@ manifest_schema = {
16
16
  "effects": {"$ref": "#/$defs/component"},
17
17
  "views": {"$ref": "#/$defs/component"},
18
18
  "applications": {"$ref": "#/$defs/applications"},
19
+ "questionnaires": {"$ref": "#/$defs/questionnaires"},
19
20
  },
20
21
  "additionalProperties": False,
21
22
  "minProperties": 1,
@@ -101,5 +102,16 @@ manifest_schema = {
101
102
  "additionalProperties": False,
102
103
  },
103
104
  },
105
+ "questionnaires": {
106
+ "type": "array",
107
+ "items": {
108
+ "type": "object",
109
+ "properties": {
110
+ "template": {"type": "string"},
111
+ },
112
+ "required": ["template"],
113
+ "additionalProperties": False,
114
+ },
115
+ },
104
116
  },
105
117
  }
canvas_sdk/__init__.py CHANGED
@@ -1,7 +1,9 @@
1
1
  import os
2
+ import sys
2
3
 
3
4
  os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
4
5
 
5
- import django
6
+ if "mypy" not in sys.argv[0]:
7
+ import django
6
8
 
7
- django.setup()
9
+ django.setup()
@@ -1,9 +1,11 @@
1
1
  from typing import Literal
2
+ from uuid import UUID
2
3
 
3
4
  from pydantic_core import InitErrorDetails
4
5
 
5
6
  from canvas_sdk.commands.base import _BaseCommand
6
7
  from canvas_sdk.commands.constants import Coding
8
+ from canvas_sdk.v1.data import ReasonForVisitSettingCoding
7
9
 
8
10
 
9
11
  class ReasonForVisitCommand(_BaseCommand):
@@ -13,8 +15,7 @@ class ReasonForVisitCommand(_BaseCommand):
13
15
  key = "reasonForVisit"
14
16
 
15
17
  structured: bool = False
16
- # how do we make sure that coding is a valid rfv coding from their home-app?
17
- coding: Coding | None = None
18
+ coding: Coding | UUID | str | None = None
18
19
  comment: str | None = None
19
20
 
20
21
  def _get_error_details(
@@ -27,6 +28,16 @@ class ReasonForVisitCommand(_BaseCommand):
27
28
  "value", "Structured RFV should have a coding.", self.coding
28
29
  )
29
30
  )
31
+
32
+ if self.coding:
33
+ if isinstance(self.coding, str | UUID):
34
+ query = {"id": self.coding}
35
+ error_message = f"ReasonForVisitSettingCoding with id {self.coding} does not exist."
36
+ else:
37
+ query = {"code": self.coding["code"], "system": self.coding["system"]}
38
+ error_message = f"ReasonForVisitSettingCoding with code {self.coding['code']} and system {self.coding['system']} does not exist."
39
+ if not ReasonForVisitSettingCoding.objects.filter(**query).exists():
40
+ errors.append(self._create_error_detail("value", error_message, self.coding))
30
41
  return errors
31
42
 
32
43
  @classmethod
@@ -40,4 +51,8 @@ class ReasonForVisitCommand(_BaseCommand):
40
51
  @property
41
52
  def values(self) -> dict:
42
53
  """The ReasonForVisit command's field values."""
43
- return {"structured": self.structured, "coding": self.coding, "comment": self.comment}
54
+ return {
55
+ "structured": self.structured,
56
+ "coding": str(self.coding) if isinstance(self.coding, UUID) else self.coding,
57
+ "comment": self.comment,
58
+ }
@@ -37,6 +37,12 @@ from canvas_sdk.commands.constants import ClinicalQuantity, Coding
37
37
  runner = CliRunner()
38
38
 
39
39
 
40
+ class WrongType:
41
+ """A type to yield ValidationErrors in tests."""
42
+
43
+ wrong_field: str
44
+
45
+
40
46
  class MaskedValue:
41
47
  """A class to mask sensitive values in tests."""
42
48
 
@@ -97,6 +103,8 @@ def fake(field_props: dict, Command: type[_BaseCommand]) -> Any:
97
103
  return Coding(system=random_string(), code=random_string(), display=random_string())
98
104
  case "ClinicalQuantity":
99
105
  return ClinicalQuantity(representative_ndc="ndc", ncpdp_quantity_qualifier_code="code")
106
+ case "WrongType":
107
+ return WrongType()
100
108
  if t[0].isupper():
101
109
  return random.choice(list(getattr(Command, t)))
102
110
 
@@ -108,7 +116,8 @@ def raises_wrong_type_error(
108
116
  """Test that the correct error is raised when the wrong type is passed to a field."""
109
117
  field_props = Command.model_json_schema()["properties"][field]
110
118
  field_type = get_field_type(field_props)
111
- wrong_field_type = "integer" if field_type == "string" else "string"
119
+
120
+ wrong_field_type = "WrongType"
112
121
 
113
122
  with pytest.raises(ValidationError) as e1:
114
123
  err_kwargs = {field: fake({"type": wrong_field_type}, Command)}
@@ -122,8 +131,11 @@ def raises_wrong_type_error(
122
131
  setattr(cmd, field, err_value)
123
132
  err_msg2 = repr(e2.value)
124
133
 
125
- assert f"1 validation error for {Command.__name__}\n{field}" in err_msg1
126
- assert f"1 validation error for {Command.__name__}\n{field}" in err_msg2
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
127
139
 
128
140
  field_type = (
129
141
  "dictionary" if field_type == "Coding" or field_type == "ClinicalQuantity" else field_type
@@ -1,3 +1,5 @@
1
+ import uuid
2
+
1
3
  import pytest
2
4
  from pydantic import ValidationError
3
5
  from typer.testing import CliRunner
@@ -91,24 +93,21 @@ def test_command_raises_generic_error_when_kwarg_given_incorrect_type(
91
93
 
92
94
 
93
95
  @pytest.mark.parametrize(
94
- "Command,err_kwargs,err_msg,valid_kwargs",
96
+ "Command,err_kwargs,valid_kwargs",
95
97
  [
96
98
  (
97
99
  PlanCommand,
98
100
  {"narrative": "yo", "note_uuid": 1},
99
- "1 validation error for PlanCommand\nnote_uuid\n Input should be a valid string [type=string_type",
100
101
  {"narrative": "yo", "note_uuid": "00000000-0000-0000-0000-000000000000"},
101
102
  ),
102
103
  (
103
104
  PlanCommand,
104
105
  {"narrative": "yo", "note_uuid": "5", "command_uuid": 5},
105
- "1 validation error for PlanCommand\ncommand_uuid\n Input should be a valid string [type=string_type",
106
106
  {"narrative": "yo", "note_uuid": "5", "command_uuid": "5"},
107
107
  ),
108
108
  (
109
109
  ReasonForVisitCommand,
110
110
  {"note_uuid": "00000000-0000-0000-0000-000000000000", "structured": True},
111
- "1 validation error for ReasonForVisitCommand\n Structured RFV should have a coding",
112
111
  {
113
112
  "note_uuid": "00000000-0000-0000-0000-000000000000",
114
113
  "structured": False,
@@ -120,7 +119,6 @@ def test_command_raises_generic_error_when_kwarg_given_incorrect_type(
120
119
  "note_uuid": "00000000-0000-0000-0000-000000000000",
121
120
  "coding": {"code": "x"},
122
121
  },
123
- "1 validation error for ReasonForVisitCommand\ncoding.system\n Field required [type=missing",
124
122
  {"note_uuid": "00000000-0000-0000-0000-000000000000"},
125
123
  ),
126
124
  (
@@ -129,7 +127,6 @@ def test_command_raises_generic_error_when_kwarg_given_incorrect_type(
129
127
  "note_uuid": "00000000-0000-0000-0000-000000000000",
130
128
  "coding": {"code": 1, "system": "y"},
131
129
  },
132
- "1 validation error for ReasonForVisitCommand\ncoding.code\n Input should be a valid string [type=string_type",
133
130
  {"note_uuid": "00000000-0000-0000-0000-000000000000"},
134
131
  ),
135
132
  (
@@ -138,7 +135,6 @@ def test_command_raises_generic_error_when_kwarg_given_incorrect_type(
138
135
  "note_uuid": "00000000-0000-0000-0000-000000000000",
139
136
  "coding": {"code": None, "system": "y"},
140
137
  },
141
- "1 validation error for ReasonForVisitCommand\ncoding.code\n Input should be a valid string [type=string_type",
142
138
  {"note_uuid": "00000000-0000-0000-0000-000000000000"},
143
139
  ),
144
140
  (
@@ -147,7 +143,6 @@ def test_command_raises_generic_error_when_kwarg_given_incorrect_type(
147
143
  "note_uuid": "00000000-0000-0000-0000-000000000000",
148
144
  "coding": {"system": "y"},
149
145
  },
150
- "1 validation error for ReasonForVisitCommand\ncoding.code\n Field required [type=missing",
151
146
  {"note_uuid": "00000000-0000-0000-0000-000000000000"},
152
147
  ),
153
148
  (
@@ -156,7 +151,6 @@ def test_command_raises_generic_error_when_kwarg_given_incorrect_type(
156
151
  "note_uuid": "00000000-0000-0000-0000-000000000000",
157
152
  "coding": {"code": "x", "system": 1},
158
153
  },
159
- "1 validation error for ReasonForVisitCommand\ncoding.system\n Input should be a valid string [type=string_type",
160
154
  {"note_uuid": "00000000-0000-0000-0000-000000000000"},
161
155
  ),
162
156
  (
@@ -165,7 +159,6 @@ def test_command_raises_generic_error_when_kwarg_given_incorrect_type(
165
159
  "note_uuid": "00000000-0000-0000-0000-000000000000",
166
160
  "coding": {"code": "x", "system": None},
167
161
  },
168
- "1 validation error for ReasonForVisitCommand\ncoding.system\n Input should be a valid string [type=string_type",
169
162
  {"note_uuid": "00000000-0000-0000-0000-000000000000"},
170
163
  ),
171
164
  (
@@ -174,7 +167,6 @@ def test_command_raises_generic_error_when_kwarg_given_incorrect_type(
174
167
  "note_uuid": "00000000-0000-0000-0000-000000000000",
175
168
  "coding": {"code": "x", "system": "y", "display": 1},
176
169
  },
177
- "1 validation error for ReasonForVisitCommand\ncoding.display\n Input should be a valid string [type=string_type",
178
170
  {"note_uuid": "00000000-0000-0000-0000-000000000000"},
179
171
  ),
180
172
  ],
@@ -182,25 +174,24 @@ def test_command_raises_generic_error_when_kwarg_given_incorrect_type(
182
174
  def test_command_raises_specific_error_when_kwarg_given_incorrect_type(
183
175
  Command: type[PlanCommand] | type[ReasonForVisitCommand],
184
176
  err_kwargs: dict,
185
- err_msg: str,
186
177
  valid_kwargs: dict,
187
178
  ) -> None:
188
179
  """Test that Command raises a specific error when a kwarg is given an incorrect type."""
189
- with pytest.raises(ValidationError) as e1:
180
+ with pytest.raises(ValidationError):
190
181
  cmd = Command(**err_kwargs)
191
182
  cmd.originate()
183
+ cmd.command_uuid = str(uuid.uuid4())
192
184
  cmd.edit()
193
- assert err_msg in repr(e1.value)
194
185
 
195
186
  cmd = Command(**valid_kwargs)
196
187
  if len(err_kwargs) < len(valid_kwargs):
197
188
  return
198
189
  key, value = list(err_kwargs.items())[-1]
199
- with pytest.raises(ValidationError) as e2:
190
+ with pytest.raises(ValidationError):
200
191
  setattr(cmd, key, value)
201
192
  cmd.originate()
193
+ cmd.command_uuid = str(uuid.uuid4())
202
194
  cmd.edit()
203
- assert err_msg in repr(e2.value)
204
195
 
205
196
 
206
197
  @pytest.mark.parametrize(
@@ -0,0 +1,3 @@
1
+ from .utils import from_yaml as questionnaire_from_yaml
2
+
3
+ __all__ = ("questionnaire_from_yaml",)
File without changes
@@ -0,0 +1,74 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ import pytest
5
+ import yaml
6
+
7
+ from canvas_sdk.effects import Effect
8
+ from canvas_sdk.events import Event, EventRequest, EventType
9
+ from canvas_sdk.questionnaires import questionnaire_from_yaml
10
+ from plugin_runner.plugin_runner import LOADED_PLUGINS
11
+ from settings import PLUGIN_DIRECTORY
12
+
13
+
14
+ @pytest.mark.parametrize("install_test_plugin", ["test_load_questionnaire"], indirect=True)
15
+ def test_from_yaml_valid_questionnaire(install_test_plugin: Path, load_test_plugins: None) -> None:
16
+ """Test that the from_yaml function loads a valid questionnaire."""
17
+ plugin = LOADED_PLUGINS[
18
+ "test_load_questionnaire:test_load_questionnaire.protocols.my_protocol:ValidQuestionnaire"
19
+ ]
20
+ result: list[Effect] = plugin["class"](Event(EventRequest(type=EventType.UNKNOWN))).compute()
21
+
22
+ assert (
23
+ yaml.load(
24
+ (
25
+ Path(PLUGIN_DIRECTORY)
26
+ / "test_load_questionnaire/questionnaires/example_questionnaire.yml"
27
+ )
28
+ .resolve()
29
+ .read_text(),
30
+ Loader=yaml.SafeLoader,
31
+ ).items()
32
+ <= json.loads(result[0].payload).items()
33
+ )
34
+
35
+
36
+ @pytest.mark.parametrize("install_test_plugin", ["test_load_questionnaire"], indirect=True)
37
+ def test_from_yaml_invalid_questionnaire(
38
+ install_test_plugin: Path, load_test_plugins: None
39
+ ) -> None:
40
+ """Test that the from_yaml function raises an error for invalid questionnaires."""
41
+ plugin = LOADED_PLUGINS[
42
+ "test_load_questionnaire:test_load_questionnaire.protocols.my_protocol:InvalidQuestionnaire"
43
+ ]
44
+ with pytest.raises(FileNotFoundError):
45
+ plugin["class"](Event(EventRequest(type=EventType.UNKNOWN))).compute()
46
+
47
+
48
+ @pytest.mark.parametrize("install_test_plugin", ["test_load_questionnaire"], indirect=True)
49
+ def test_from_yaml_forbidden_questionnaire(
50
+ install_test_plugin: Path, load_test_plugins: None
51
+ ) -> None:
52
+ """Test that the from_yaml function raises an error for a questionnaire outside plugin package."""
53
+ plugin = LOADED_PLUGINS[
54
+ "test_load_questionnaire:test_load_questionnaire.protocols.my_protocol:ForbiddenQuestionnaire"
55
+ ]
56
+ with pytest.raises(PermissionError):
57
+ plugin["class"](Event(EventRequest(type=EventType.UNKNOWN))).compute()
58
+
59
+
60
+ def test_from_yaml_non_plugin_caller() -> None:
61
+ """Test that the from_yaml function returns None when called outside a plugin."""
62
+ assert questionnaire_from_yaml("questionnaires/example_questionnaire.yml") is None
63
+
64
+
65
+ @pytest.mark.parametrize("install_test_plugin", ["test_load_questionnaire"], indirect=True)
66
+ def test_from_yaml_sets_default_values(install_test_plugin: Path) -> None:
67
+ """Test that the from_yaml function sets default values for properties."""
68
+ globals()["__is_plugin__"] = True
69
+ globals()["__name__"] = "test_load_questionnaire"
70
+
71
+ definition = questionnaire_from_yaml("questionnaires/example_questionnaire.yml")
72
+
73
+ assert definition is not None
74
+ assert definition["display_results_in_social_history_section"] is False
@@ -0,0 +1,117 @@
1
+ import functools
2
+ import json
3
+ from collections.abc import Generator
4
+ from pathlib import Path
5
+ from typing import Any, TypedDict
6
+
7
+ import yaml
8
+ from jsonschema import Draft7Validator, validators
9
+
10
+ from canvas_sdk.utils.plugins import plugin_only
11
+
12
+
13
+ class Response(TypedDict):
14
+ """A Response of a Questionnaire."""
15
+
16
+ name: str
17
+ code: str
18
+ code_description: str
19
+ value: str
20
+
21
+
22
+ class Question(TypedDict):
23
+ """A Question of a Questionnaire."""
24
+
25
+ code_system: str
26
+ code: str
27
+ code_description: str
28
+ content: str
29
+ responses_code_system: str
30
+ responses_type: str
31
+ display_result_in_social_history_section: bool
32
+ responses: list[Response]
33
+
34
+
35
+ class QuestionnaireConfig(TypedDict):
36
+ """A Questionnaire configuration."""
37
+
38
+ name: str
39
+ form_type: str
40
+ code_system: str
41
+ code: str
42
+ can_originate_in_charting: bool
43
+ prologue: str
44
+ display_results_in_social_history_section: bool
45
+ questions: list[Question]
46
+
47
+
48
+ def extend_with_defaults(validator_class: type[Draft7Validator]) -> type[Draft7Validator]:
49
+ """Extend a Draft7Validator with default values for properties."""
50
+ validate_properties = validator_class.VALIDATORS["properties"]
51
+
52
+ def set_defaults(
53
+ validator: Draft7Validator,
54
+ properties: dict[str, Any],
55
+ instance: dict[str, Any],
56
+ schema: dict[str, Any],
57
+ ) -> Generator[Any, None, None]:
58
+ for property, subschema in properties.items():
59
+ if "default" in subschema:
60
+ instance.setdefault(property, subschema["default"])
61
+
62
+ yield from validate_properties(
63
+ validator,
64
+ properties,
65
+ instance,
66
+ schema,
67
+ )
68
+
69
+ return validators.extend(
70
+ validator_class,
71
+ {"properties": set_defaults},
72
+ )
73
+
74
+
75
+ ExtendedDraft7Validator = extend_with_defaults(Draft7Validator)
76
+
77
+
78
+ @plugin_only
79
+ def from_yaml(questionnaire_name: str, **kwargs: Any) -> QuestionnaireConfig | None:
80
+ """Load a Questionnaire configuration from a YAML file.
81
+
82
+ Args:
83
+ questionnaire_name (str): The path to the questionnaire file, relative to the plugin package.
84
+ If the path starts with a forward slash ("/"), it will be stripped during resolution.
85
+ kwargs (Any): Additional keyword arguments.
86
+
87
+ Returns:
88
+ QuestionnaireConfig: The loaded Questionnaire configuration.
89
+
90
+ Raises:
91
+ FileNotFoundError: If the questionnaire file does not exist within the plugin's directory
92
+ or if the resolved path is invalid.
93
+ PermissionError: If the resolved path is outside the plugin's directory.
94
+ ValidationError: If the questionnaire file does not conform to the JSON schema.
95
+ """
96
+ plugin_dir = kwargs["plugin_dir"]
97
+ questionnaire_config_path = Path(plugin_dir / questionnaire_name.lstrip("/")).resolve()
98
+
99
+ if not questionnaire_config_path.is_relative_to(plugin_dir):
100
+ raise PermissionError(f"Invalid Questionnaire '{questionnaire_name}'")
101
+ elif not questionnaire_config_path.exists():
102
+ raise FileNotFoundError(f"Questionnaire {questionnaire_name} not found.")
103
+
104
+ questionnaire_config = yaml.load(questionnaire_config_path.read_text(), Loader=yaml.SafeLoader)
105
+ ExtendedDraft7Validator(json_schema()).validate(questionnaire_config)
106
+
107
+ return questionnaire_config
108
+
109
+
110
+ @functools.cache
111
+ def json_schema() -> dict[str, Any]:
112
+ """Reads the JSON schema for a Questionnaire Config."""
113
+ schema = json.loads(
114
+ (Path(__file__).resolve().parent.parent.parent / "schemas/questionnaire.json").read_text()
115
+ )
116
+
117
+ return schema
@@ -1,13 +1,15 @@
1
- import inspect
2
1
  from pathlib import Path
3
2
  from typing import Any
4
3
 
5
4
  from django.template import Context, Template
6
5
 
7
- from settings import PLUGIN_DIRECTORY
6
+ from canvas_sdk.utils.plugins import plugin_only
8
7
 
9
8
 
10
- def render_to_string(template_name: str, context: dict[str, Any] | None = None) -> str | None:
9
+ @plugin_only
10
+ def render_to_string(
11
+ template_name: str, context: dict[str, Any] | None = None, **kwargs: Any
12
+ ) -> str | None:
11
13
  """Load a template and render it with the given context.
12
14
 
13
15
  Args:
@@ -15,6 +17,7 @@ def render_to_string(template_name: str, context: dict[str, Any] | None = None)
15
17
  If the path starts with a forward slash ("/"), it will be stripped during resolution.
16
18
  context (dict[str, Any] | None): A dictionary of variables to pass to the template
17
19
  for rendering. Defaults to None, which uses an empty context.
20
+ kwargs (Any): Additional keyword arguments.
18
21
 
19
22
  Returns:
20
23
  str: The rendered template as a string.
@@ -23,15 +26,7 @@ def render_to_string(template_name: str, context: dict[str, Any] | None = None)
23
26
  FileNotFoundError: If the template file does not exist within the plugin's directory
24
27
  or if the resolved path is invalid.
25
28
  """
26
- plugins_dir = Path(PLUGIN_DIRECTORY).resolve()
27
- current_frame = inspect.currentframe()
28
- caller = current_frame.f_back if current_frame else None
29
-
30
- if not caller or "__is_plugin__" not in caller.f_globals:
31
- return None
32
-
33
- plugin_name = caller.f_globals["__name__"].split(".")[0]
34
- plugin_dir = plugins_dir / plugin_name
29
+ plugin_dir = kwargs["plugin_dir"]
35
30
  template_path = Path(plugin_dir / template_name.lstrip("/")).resolve()
36
31
 
37
32
  if not template_path.is_relative_to(plugin_dir):
@@ -0,0 +1,25 @@
1
+ import inspect
2
+ from collections.abc import Callable
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from settings import PLUGIN_DIRECTORY
7
+
8
+
9
+ def plugin_only(func: Callable[..., Any]) -> Callable[..., Any]:
10
+ """Decorator to restrict a function's execution to plugins only."""
11
+
12
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
13
+ current_frame = inspect.currentframe()
14
+ caller = current_frame.f_back if current_frame else None
15
+
16
+ if not caller or "__is_plugin__" not in caller.f_globals:
17
+ return None
18
+
19
+ plugin_name = caller.f_globals["__name__"].split(".")[0]
20
+ plugin_dir = Path(PLUGIN_DIRECTORY) / plugin_name
21
+ kwargs["plugin_dir"] = plugin_dir.resolve()
22
+
23
+ return func(*args, **kwargs)
24
+
25
+ return wrapper
@@ -1,7 +1,7 @@
1
1
  from .allergy_intolerance import AllergyIntolerance, AllergyIntoleranceCoding
2
2
  from .appointment import Appointment
3
3
  from .assessment import Assessment
4
- from .billing import BillingLineItem
4
+ from .billing import BillingLineItem, BillingLineItemModifier
5
5
  from .care_team import CareTeamMembership, CareTeamRole
6
6
  from .command import Command
7
7
  from .condition import Condition, ConditionCoding
@@ -48,6 +48,7 @@ from .questionnaire import (
48
48
  ResponseOption,
49
49
  ResponseOptionSet,
50
50
  )
51
+ from .reason_for_visit import ReasonForVisitSettingCoding
51
52
  from .staff import Staff
52
53
  from .task import Task, TaskComment, TaskLabel, TaskTaskLabel
53
54
  from .user import CanvasUser
@@ -58,6 +59,7 @@ __all__ = [
58
59
  "AllergyIntoleranceCoding",
59
60
  "Assessment",
60
61
  "BillingLineItem",
62
+ "BillingLineItemModifier",
61
63
  "CanvasUser",
62
64
  "CareTeamMembership",
63
65
  "CareTeamRole",
@@ -103,6 +105,7 @@ __all__ = [
103
105
  "Question",
104
106
  "Questionnaire",
105
107
  "QuestionnaireQuestionMap",
108
+ "ReasonForVisitSettingCoding",
106
109
  "ResponseOption",
107
110
  "ResponseOptionSet",
108
111
  "Staff",
@@ -44,10 +44,16 @@ class BillingLineItem(models.Model):
44
44
  created = models.DateTimeField()
45
45
  modified = models.DateTimeField()
46
46
  note = models.ForeignKey(
47
- "v1.Note", on_delete=models.DO_NOTHING, related_name="billing_line_items", null=True
47
+ "v1.Note",
48
+ on_delete=models.DO_NOTHING,
49
+ related_name="billing_line_items",
50
+ null=True,
48
51
  )
49
52
  patient = models.ForeignKey(
50
- "v1.Patient", on_delete=models.DO_NOTHING, related_name="billing_line_items", null=True
53
+ "v1.Patient",
54
+ on_delete=models.DO_NOTHING,
55
+ related_name="billing_line_items",
56
+ null=True,
51
57
  )
52
58
  cpt = models.CharField()
53
59
  charge = models.DecimalField()
@@ -56,3 +62,24 @@ class BillingLineItem(models.Model):
56
62
  command_type = models.CharField()
57
63
  command_id = models.IntegerField()
58
64
  status = models.CharField(choices=BillingLineItemStatus.choices)
65
+
66
+
67
+ class BillingLineItemModifier(models.Model):
68
+ """BillingLineItemModifier."""
69
+
70
+ class Meta:
71
+ managed = False
72
+ db_table = "canvas_sdk_data_api_billinglineitemmodifier_001"
73
+
74
+ dbid = models.BigIntegerField(primary_key=True)
75
+ system = models.CharField()
76
+ version = models.CharField()
77
+ code = models.CharField()
78
+ display = models.CharField()
79
+ user_selected = models.BooleanField()
80
+ line_item = models.ForeignKey(
81
+ "v1.BillingLineItem",
82
+ on_delete=models.DO_NOTHING,
83
+ related_name="modifiers",
84
+ null=True,
85
+ )
@@ -46,7 +46,7 @@ class CoverageRelationshipCode(models.TextChoices):
46
46
  """CoverageRelationshipCode."""
47
47
 
48
48
  SELF = "18", "Self"
49
- SPOUSE = "01" "Spouse"
49
+ SPOUSE = "01", "Spouse"
50
50
  CHILD_INSURED_HAS_FINANCIAL_RESP = "19", "Natural Child, insured has financial responsibility"
51
51
  CHILD_HAS_FINANCIAL_RESP = "43", "Natural Child, insured does not have financial responsibility"
52
52
  STEP_CHILD = "17", "Step Child"
@@ -0,0 +1,22 @@
1
+ from django.contrib.postgres.fields import ArrayField
2
+ from django.db import models
3
+
4
+
5
+ class ReasonForVisitSettingCoding(models.Model):
6
+ """ReasonForVisitSettingCoding."""
7
+
8
+ class Meta:
9
+ managed = False
10
+ db_table = "canvas_sdk_data_api_reasonforvisitsettingcoding_001"
11
+
12
+ objects: models.Manager["ReasonForVisitSettingCoding"]
13
+
14
+ id = models.UUIDField()
15
+ dbid = models.BigIntegerField(primary_key=True)
16
+
17
+ code = models.CharField()
18
+ display = models.CharField()
19
+ system = models.CharField()
20
+ version = models.CharField()
21
+
22
+ duration = ArrayField(models.DurationField())
@@ -6,7 +6,7 @@ import tempfile
6
6
  from collections.abc import Generator
7
7
  from contextlib import contextmanager
8
8
  from pathlib import Path
9
- from typing import Any, TypedDict
9
+ from typing import TypedDict, cast
10
10
  from urllib import parse
11
11
 
12
12
  import psycopg
@@ -30,41 +30,32 @@ from settings import (
30
30
  UPLOAD_TO_PREFIX = "plugins"
31
31
 
32
32
 
33
- def get_database_dict_from_url() -> dict[str, Any]:
34
- """Creates a psycopg ready dictionary from the home-app database URL."""
35
- parsed_url = parse.urlparse(os.getenv("DATABASE_URL"))
36
- db_name = parsed_url.path[1:]
37
- return {
38
- "dbname": db_name,
39
- "user": parsed_url.username,
40
- "password": parsed_url.password,
41
- "host": parsed_url.hostname,
42
- "port": parsed_url.port,
43
- }
33
+ def open_database_connection() -> Connection:
34
+ """Opens a psycopg connection to the home-app database.
44
35
 
36
+ When running within Aptible, use the database URL, otherwise pull from
37
+ the environment variables.
38
+ """
39
+ if os.getenv("DATABASE_URL"):
40
+ parsed_url = parse.urlparse(os.getenv("DATABASE_URL"))
41
+
42
+ return psycopg.connect(
43
+ dbname=cast(str, parsed_url.path[1:]),
44
+ user=cast(str, parsed_url.username),
45
+ password=cast(str, parsed_url.password),
46
+ host=cast(str, parsed_url.hostname),
47
+ port=parsed_url.port,
48
+ )
45
49
 
46
- def get_database_dict_from_env() -> dict[str, Any]:
47
- """Creates a psycopg ready dictionary from the environment variables."""
48
50
  APP_NAME = os.getenv("APP_NAME")
49
51
 
50
- return {
51
- "dbname": APP_NAME,
52
- "user": os.getenv("DB_USERNAME", "app"),
53
- "password": os.getenv("DB_PASSWORD", "app"),
54
- "host": os.getenv("DB_HOST", f"{APP_NAME}-db"),
55
- "port": os.getenv("DB_PORT", "5432"),
56
- }
57
-
58
-
59
- def open_database_connection() -> Connection:
60
- """Opens a psycopg connection to the home-app database."""
61
- # When running within Aptible, use the database URL, otherwise pull from the environment variables.
62
- if os.getenv("DATABASE_URL"):
63
- database_dict = get_database_dict_from_url()
64
- else:
65
- database_dict = get_database_dict_from_env()
66
- conn = psycopg.connect(**database_dict)
67
- return conn
52
+ return psycopg.connect(
53
+ dbname=APP_NAME,
54
+ user=os.getenv("DB_USERNAME", "app"),
55
+ password=os.getenv("DB_PASSWORD", "app"),
56
+ host=os.getenv("DB_HOST", f"{APP_NAME}-db"),
57
+ port=os.getenv("DB_PORT", "5432"),
58
+ )
68
59
 
69
60
 
70
61
  class PluginAttributes(TypedDict):
plugin_runner/sandbox.py CHANGED
@@ -45,6 +45,7 @@ ALLOWED_MODULES = frozenset(
45
45
  "canvas_sdk.events",
46
46
  "canvas_sdk.handlers",
47
47
  "canvas_sdk.protocols",
48
+ "canvas_sdk.questionnaires",
48
49
  "canvas_sdk.utils",
49
50
  "canvas_sdk.templates",
50
51
  "canvas_sdk.v1",
@@ -173,12 +174,12 @@ class Sandbox:
173
174
  ):
174
175
  self.warn(
175
176
  node,
176
- f'"{name}" is an invalid variable name because it ' 'starts with "_"',
177
+ f'"{name}" is an invalid variable name because it starts with "_"',
177
178
  )
178
179
  elif name.endswith("__roles__"):
179
180
  self.error(
180
181
  node,
181
- f'"{name}" is an invalid variable name because ' 'it ends with "__roles__".',
182
+ f'"{name}" is an invalid variable name because it ends with "__roles__".',
182
183
  )
183
184
  elif name in FORBIDDEN_FUNC_NAMES:
184
185
  self.error(node, f'"{name}" is a reserved name.')
@@ -215,21 +216,20 @@ class Sandbox:
215
216
  if node.attr.startswith("_") and node.attr != "_":
216
217
  self.warn(
217
218
  node,
218
- f'"{node.attr}" is an invalid attribute name because it starts ' 'with "_".',
219
+ f'"{node.attr}" is an invalid attribute name because it starts with "_".',
219
220
  )
220
221
 
221
222
  if node.attr.endswith("__roles__"):
222
223
  self.error(
223
224
  node,
224
- f'"{node.attr}" is an invalid attribute name because it ends '
225
- 'with "__roles__".',
225
+ f'"{node.attr}" is an invalid attribute name because it ends with "__roles__".',
226
226
  )
227
227
 
228
228
  if isinstance(node.ctx, ast.Load):
229
229
  node = self.node_contents_visit(node)
230
230
  new_node = ast.Call(
231
231
  func=ast.Name("_getattr_", ast.Load()),
232
- args=[node.value, ast.Str(node.attr)],
232
+ args=[node.value, ast.Constant(node.attr)],
233
233
  keywords=[],
234
234
  )
235
235
 
@@ -0,0 +1,52 @@
1
+ {
2
+ "sdk_version": "0.1.4",
3
+ "plugin_version": "0.0.1",
4
+ "name": "test_load_questionnaire",
5
+ "description": "Edit the description in CANVAS_MANIFEST.json",
6
+ "components": {
7
+ "protocols": [
8
+ {
9
+ "class": "test_load_questionnaire.protocols.my_protocol:ValidQuestionnaire",
10
+ "description": "A protocol that does xyz...",
11
+ "data_access": {
12
+ "event": "",
13
+ "read": [],
14
+ "write": []
15
+ }
16
+ },
17
+ {
18
+ "class": "test_load_questionnaire.protocols.my_protocol:InvalidQuestionnaire",
19
+ "description": "A protocol that does xyz...",
20
+ "data_access": {
21
+ "event": "",
22
+ "read": [],
23
+ "write": []
24
+ }
25
+ },
26
+ {
27
+ "class": "test_load_questionnaire.protocols.my_protocol:ForbiddenQuestionnaire",
28
+ "description": "A protocol that does xyz...",
29
+ "data_access": {
30
+ "event": "",
31
+ "read": [],
32
+ "write": []
33
+ }
34
+ }
35
+ ],
36
+ "commands": [],
37
+ "content": [],
38
+ "effects": [],
39
+ "views": [],
40
+ "questionnaires": [
41
+ {
42
+ "template": "questionnaires/example_questionnaire.yml"
43
+ }
44
+ ]
45
+ },
46
+ "secrets": [],
47
+ "tags": {},
48
+ "references": [],
49
+ "license": "",
50
+ "diagram": false,
51
+ "readme": "./README.md"
52
+ }
@@ -0,0 +1,11 @@
1
+ test_load_questionnaire
2
+ ====================
3
+
4
+ ## Description
5
+
6
+ A description of this plugin
7
+
8
+ ### Important Note!
9
+
10
+ The CANVAS_MANIFEST.json is used when installing your plugin. Please ensure it
11
+ gets updated if you add, remove, or rename protocols.
@@ -0,0 +1,39 @@
1
+ import json
2
+
3
+ from canvas_sdk.effects import Effect
4
+ from canvas_sdk.events import EventType
5
+ from canvas_sdk.handlers import BaseHandler
6
+ from canvas_sdk.questionnaires import questionnaire_from_yaml
7
+
8
+
9
+ class ValidQuestionnaire(BaseHandler):
10
+ """You should put a helpful description of this protocol's behavior here."""
11
+
12
+ RESPONDS_TO = [EventType.Name(EventType.UNKNOWN)]
13
+
14
+ def compute(self) -> list[Effect]:
15
+ """This method gets called when an event of the type RESPONDS_TO is fired."""
16
+ config = questionnaire_from_yaml("questionnaires/example_questionnaire.yml")
17
+ return [Effect(payload=json.dumps(config))]
18
+
19
+
20
+ class InvalidQuestionnaire(BaseHandler):
21
+ """You should put a helpful description of this protocol's behavior here."""
22
+
23
+ RESPONDS_TO = [EventType.Name(EventType.UNKNOWN)]
24
+
25
+ def compute(self) -> list[Effect]:
26
+ """This method gets called when an event of the type RESPONDS_TO is fired."""
27
+ questionnaire_from_yaml("questionnaires/example_questionnaire1.yml")
28
+ return []
29
+
30
+
31
+ class ForbiddenQuestionnaire(BaseHandler):
32
+ """You should put a helpful description of this protocol's behavior here."""
33
+
34
+ RESPONDS_TO = [EventType.Name(EventType.UNKNOWN)]
35
+
36
+ def compute(self) -> list[Effect]:
37
+ """This method gets called when an event of the type RESPONDS_TO is fired."""
38
+ questionnaire_from_yaml("../../questionnaires/example_questionnaire.yml")
39
+ return []
@@ -0,0 +1,61 @@
1
+ # yaml-language-server: $schema=../../../../../../schemas/questionnaire.json
2
+
3
+ name: Example Name
4
+ form_type: QUES
5
+ code_system: LOINC
6
+ code: QUES_EXAMPLE_NAME
7
+ can_originate_in_charting: true
8
+ prologue: This is an example of a structured assessment with single select, multiselect, and free text responses.
9
+ questions:
10
+ - content: "This is question #1"
11
+ code_system: CPT
12
+ code: H0005
13
+ code_description: ""
14
+ responses_code_system: INTERNAL
15
+ responses_type: SING
16
+ display_result_in_social_history_section: true
17
+ responses:
18
+ - name: "Single select response #1"
19
+ code: QUES_EXAMPLE_NAME_Q1_A1
20
+ code_description: ''
21
+ value: "1"
22
+ - name: "Single select response #2"
23
+ code: QUES_EXAMPLE_NAME_Q1_A2
24
+ code_description: ''
25
+ value: "0"
26
+ - name: "Single select response #3"
27
+ code: QUES_EXAMPLE_NAME_Q1_A3
28
+ code_description: ''
29
+ value: "0"
30
+ - content: "This is question #2"
31
+ code_system: INTERNAL
32
+ code: QUES_EXAMPLE_NAME_Q2
33
+ code_description: ""
34
+ responses_code_system: ICD-10
35
+ responses_type: MULT
36
+ display_result_in_social_history_section: true
37
+ responses:
38
+ - name: "Multi select response #1"
39
+ code: F1910
40
+ code_description: ''
41
+ value: "0"
42
+ - name: "Multi select response #2"
43
+ code: QUES_EXAMPLE_NAME_Q1_A1
44
+ code_description: ''
45
+ value: "2"
46
+ - name: "Multi select response #3"
47
+ code: QUES_EXAMPLE_NAME_Q1_A2
48
+ code_description: ''
49
+ value: "0"
50
+ - content: "This is question #3"
51
+ code_system: INTERNAL
52
+ code: QUES_EXAMPLE_NAME_Q3
53
+ code_description: ""
54
+ responses_code_system: INTERNAL
55
+ responses_type: TXT
56
+ display_result_in_social_history_section: true
57
+ responses:
58
+ - name: "Free text response"
59
+ code: QUES_EXAMPLE_NAME_Q3_A1
60
+ code_description: ''
61
+ value: "This is a default pre-populated free text response."
@@ -83,9 +83,9 @@ def test_load_plugins_with_plugin_that_imports_other_modules_outside_plugin_pack
83
83
  with caplog.at_level(logging.ERROR):
84
84
  load_or_reload_plugin(install_test_plugin)
85
85
 
86
- assert any(
87
- "Error importing module" in record.message for record in caplog.records
88
- ), "log.error() was not called with the expected message."
86
+ assert any("Error importing module" in record.message for record in caplog.records), (
87
+ "log.error() was not called with the expected message."
88
+ )
89
89
 
90
90
 
91
91
  @pytest.mark.parametrize(
@@ -102,9 +102,9 @@ def test_load_plugins_with_plugin_that_imports_forbidden_modules(
102
102
  with caplog.at_level(logging.ERROR):
103
103
  load_or_reload_plugin(install_test_plugin)
104
104
 
105
- assert any(
106
- "Error importing module" in record.message for record in caplog.records
107
- ), "log.error() was not called with the expected message."
105
+ assert any("Error importing module" in record.message for record in caplog.records), (
106
+ "log.error() was not called with the expected message."
107
+ )
108
108
 
109
109
 
110
110
  @pytest.mark.parametrize(
@@ -144,9 +144,9 @@ def test_plugin_that_implicitly_imports_allowed_modules(
144
144
  ]["class"]
145
145
  class_handler(Event(EventRequest(type=EventType.UNKNOWN))).compute()
146
146
 
147
- assert any(
148
- "Hello, World!" in record.message for record in caplog.records
149
- ), "log.info() with Template.render() was not called."
147
+ assert any("Hello, World!" in record.message for record in caplog.records), (
148
+ "log.info() with Template.render() was not called."
149
+ )
150
150
 
151
151
 
152
152
  @pytest.mark.parametrize(
@@ -170,9 +170,9 @@ def test_plugin_that_implicitly_imports_forbidden_modules(
170
170
  ]["class"]
171
171
  class_handler(Event(EventRequest(type=EventType.UNKNOWN))).compute()
172
172
 
173
- assert (
174
- any("os list dir" in record.message for record in caplog.records) is False
175
- ), "log.info() with os.listdir() was called."
173
+ assert any("os list dir" in record.message for record in caplog.records) is False, (
174
+ "log.info() with os.listdir() was called."
175
+ )
176
176
 
177
177
 
178
178
  @pytest.mark.parametrize("install_test_plugin", ["example_plugin"], indirect=True)
settings.py CHANGED
@@ -1,11 +1,10 @@
1
1
  import os
2
2
  import sys
3
+ from urllib import parse
3
4
 
4
5
  from dotenv import load_dotenv
5
6
  from env_tools import env_to_bool
6
7
 
7
- from canvas_sdk.utils.db import get_database_dict_from_url
8
-
9
8
  load_dotenv()
10
9
 
11
10
  ENV = os.getenv("ENV", "development")
@@ -40,19 +39,30 @@ CANVAS_SDK_DB_HOST = os.getenv("CANVAS_SDK_DB_HOST", "home-app-db")
40
39
  CANVAS_SDK_DB_PORT = os.getenv("CANVAS_SDK_DB_PORT", "5432")
41
40
 
42
41
  if os.getenv("DATABASE_URL"):
43
- database_dict = get_database_dict_from_url()
42
+ parsed_url = parse.urlparse(os.getenv("DATABASE_URL"))
43
+
44
+ DATABASES = {
45
+ "default": {
46
+ "ENGINE": "django.db.backends.postgresql",
47
+ "NAME": parsed_url.path[1:],
48
+ "USER": os.getenv("CANVAS_SDK_DATABASE_ROLE"),
49
+ "PASSWORD": os.getenv("CANVAS_SDK_DATABASE_ROLE_PASSWORD"),
50
+ "HOST": parsed_url.hostname,
51
+ "PORT": parsed_url.port,
52
+ }
53
+ }
44
54
  else:
45
- database_dict = {
46
- "ENGINE": "django.db.backends.postgresql",
47
- "NAME": CANVAS_SDK_DB_NAME,
48
- "USER": CANVAS_SDK_DB_USERNAME,
49
- "PASSWORD": CANVAS_SDK_DB_PASSWORD,
50
- "HOST": CANVAS_SDK_DB_HOST,
51
- "PORT": CANVAS_SDK_DB_PORT,
55
+ DATABASES = {
56
+ "default": {
57
+ "ENGINE": "django.db.backends.postgresql",
58
+ "NAME": CANVAS_SDK_DB_NAME,
59
+ "USER": CANVAS_SDK_DB_USERNAME,
60
+ "PASSWORD": CANVAS_SDK_DB_PASSWORD,
61
+ "HOST": CANVAS_SDK_DB_HOST,
62
+ "PORT": CANVAS_SDK_DB_PORT,
63
+ }
52
64
  }
53
65
 
54
- DATABASES = {"default": database_dict}
55
-
56
66
  AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID", "")
57
67
  AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY", "")
58
68
  AWS_REGION = os.getenv("AWS_REGION", "us-west-2")
canvas_sdk/utils/db.py DELETED
@@ -1,17 +0,0 @@
1
- import os
2
- from typing import Any
3
- from urllib import parse
4
-
5
-
6
- def get_database_dict_from_url() -> dict[str, Any]:
7
- """Retrieves the database URL for the data module connection formatted for Django settings."""
8
- parsed_url = parse.urlparse(os.getenv("DATABASE_URL"))
9
- db_name = parsed_url.path[1:]
10
- return {
11
- "ENGINE": "django.db.backends.postgresql",
12
- "NAME": db_name,
13
- "USER": os.getenv("CANVAS_SDK_DATABASE_ROLE"),
14
- "PASSWORD": os.getenv("CANVAS_SDK_DATABASE_ROLE_PASSWORD"),
15
- "HOST": parsed_url.hostname,
16
- "PORT": parsed_url.port,
17
- }