canvas 0.16.0__py3-none-any.whl → 0.17.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of canvas might be problematic. Click here for more details.

@@ -1,39 +1,34 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: canvas
3
- Version: 0.16.0
3
+ Version: 0.17.0
4
4
  Summary: SDK to customize event-driven actions in your Canvas instance
5
- License: MIT
6
- Author: Canvas Team
7
- Author-email: engineering@canvasmedical.com
8
- Requires-Python: >=3.11,<3.13
9
- Classifier: License :: OSI Approved :: MIT License
10
- Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.11
12
- Classifier: Programming Language :: Python :: 3.12
5
+ Author-email: Canvas Team <engineering@canvasmedical.com>
6
+ License-Expression: MIT
7
+ Requires-Python: <3.13,>=3.11
13
8
  Requires-Dist: cookiecutter
14
- Requires-Dist: cron-converter (>=1.2.1,<2.0.0)
15
- Requires-Dist: deprecation (>=2.1.0,<3.0.0)
16
- Requires-Dist: django (>=5.1.1,<6.0.0)
17
- Requires-Dist: django-stubs[compatible-mypy] (>=5.1.1,<6.0.0)
18
- Requires-Dist: django-timezone-utils (>=0.15.0,<0.16.0)
19
- Requires-Dist: env-tools (>=2.4.0,<3.0.0)
20
- Requires-Dist: grpcio (>=1.60.1,<2.0.0)
21
- Requires-Dist: ipython (>=8.21.0,<9.0.0)
22
- Requires-Dist: jsonschema (>=4.21.1,<5.0.0)
9
+ Requires-Dist: cron-converter<2,>=1.2.1
10
+ Requires-Dist: deprecation<3,>=2.1.0
11
+ Requires-Dist: django-stubs[compatible-mypy]<6,>=5.1.1
12
+ Requires-Dist: django-timezone-utils<0.16,>=0.15.0
13
+ Requires-Dist: django<6,>=5.1.1
14
+ Requires-Dist: env-tools<3,>=2.4.0
15
+ Requires-Dist: grpcio<2,>=1.60.1
16
+ Requires-Dist: ipython<9,>=8.21.0
17
+ Requires-Dist: jsonschema<5,>=4.21.1
23
18
  Requires-Dist: keyring
24
- Requires-Dist: protobuf (>=4.25.3,<5.0.0)
25
- Requires-Dist: psycopg[binary] (>=3.2.2,<4.0.0)
26
- Requires-Dist: pydantic (>=2.6.1,<3.0.0)
27
- Requires-Dist: pyjwt (==2.10.1)
28
- Requires-Dist: python-dotenv (>=1.0.1,<2.0.0)
29
- Requires-Dist: rapidfuzz (>=3.10.1,<4.0.0)
30
- Requires-Dist: redis (>=5.0.4,<6.0.0)
19
+ Requires-Dist: protobuf<5,>=4.25.3
20
+ Requires-Dist: psycopg[binary]<4,>=3.2.2
21
+ Requires-Dist: pydantic<3,>=2.6.1
22
+ Requires-Dist: pyjwt==2.10.1
23
+ Requires-Dist: python-dotenv<2,>=1.0.1
24
+ Requires-Dist: rapidfuzz<4,>=3.10.1
25
+ Requires-Dist: redis<6,>=5.0.4
31
26
  Requires-Dist: requests
32
- Requires-Dist: restrictedpython (>=7.1,<9.0)
33
- Requires-Dist: statsd (>=4.0.1,<5.0.0)
27
+ Requires-Dist: restrictedpython>=8.0
28
+ Requires-Dist: statsd<5,>=4.0.1
34
29
  Requires-Dist: typer
35
- Requires-Dist: typing-extensions (>=4.8,<4.13)
36
- Requires-Dist: websocket-client (>=1.7.0,<2.0.0)
30
+ Requires-Dist: typing-extensions<4.13,>=4.8
31
+ Requires-Dist: websocket-client<2,>=1.7.0
37
32
  Description-Content-Type: text/markdown
38
33
 
39
34
  ### Getting Started
@@ -220,4 +215,3 @@ $ canvas logs [OPTIONS]
220
215
 
221
216
  - `--host TEXT`: Canvas instance to connect to
222
217
  - `--help`: Show this message and exit.
223
-
@@ -1,4 +1,8 @@
1
+ settings.py,sha256=wtQMRHsbynzEjZb5WmUMV3q27oWc6prLS9ztnEAsqMg,2829
1
2
  canvas_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ canvas_cli/conftest.py,sha256=0mEjjU7_w9nq-HCvscxFJClw-EeyQYujNLLRPssksZQ,931
4
+ canvas_cli/main.py,sha256=L6JQkt1yxy30cA3-M9v7JD8WMW4i0M5GPr9kZetAito,2728
5
+ canvas_cli/tests.py,sha256=kY3eeDL-EIlNCeeTponVeqz9Zb5S12imUUCSzEXuYFw,6527
2
6
  canvas_cli/apps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
7
  canvas_cli/apps/auth/__init__.py,sha256=gIwJ2qWvRlLqbiRkudrGqTKV-orlb8OTkG487qoRda4,105
4
8
  canvas_cli/apps/auth/tests.py,sha256=_q8YvSN1zVgWh_RyWCo4Dkme67tr0ioQ10AkfkpvTbc,5030
@@ -65,12 +69,10 @@ canvas_cli/apps/emit/event_fixtures/VITAL_SIGN_UPDATED.ndjson,sha256=Er0aUWIYkqb
65
69
  canvas_cli/apps/logs/__init__.py,sha256=ehY9SRb6nBw81xZF50yyBlUZJtNR2VeVSNI5sFuWJ7o,64
66
70
  canvas_cli/apps/logs/logs.py,sha256=BFpZ-2OF2Rs1EMLePo5UjqC9fKQeqm8qZobNTFNCL_M,1972
67
71
  canvas_cli/apps/plugin/__init__.py,sha256=G_nLsu6cdko5OjatnbqUyEboGcNlGGLwpZmCBxMKdfo,236
68
- canvas_cli/apps/plugin/plugin.py,sha256=YyQbAiB-10JSGCPLzNMjw4Dg1AzE9zq5yvAIAQ36S7M,15578
72
+ canvas_cli/apps/plugin/plugin.py,sha256=-LdjxtO3t3CIasoh7A1-_7bcFN04TkjmfhbRGHNeZH0,15566
69
73
  canvas_cli/apps/plugin/tests.py,sha256=SsYeYY25ly9TMn-nkJEZjLaPCyFbT4vs1sN_FnQbJ5U,2746
70
74
  canvas_cli/apps/run_plugins/__init__.py,sha256=iAMgX_6D3CdjQodGx_azwhSjouaxquOm8Z8QVXnlTFE,117
71
75
  canvas_cli/apps/run_plugins/run_plugins.py,sha256=qsf6-UhFAZpIL-1C50fzSoIwXMsZISxg2fxzM46UHTA,384
72
- canvas_cli/conftest.py,sha256=0mEjjU7_w9nq-HCvscxFJClw-EeyQYujNLLRPssksZQ,931
73
- canvas_cli/main.py,sha256=L6JQkt1yxy30cA3-M9v7JD8WMW4i0M5GPr9kZetAito,2728
74
76
  canvas_cli/templates/plugins/application/cookiecutter.json,sha256=cI4Wpj68TkKeBP3P16PrjKacTHzsTIpl_rDdzyUpwz4,129
75
77
  canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/CANVAS_MANIFEST.json,sha256=rIB41kNzzRV9QyX1ssHNi4n4wXyuXeBy2gVG83hxsOA,908
76
78
  canvas_cli/templates/plugins/application/{{ cookiecutter.__project_slug }}/README.md,sha256=3QKoJQq3YmdplGnDOBMsLCJ3Ya1_aKjoz-QiWc4QfjA,291
@@ -82,7 +84,6 @@ canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/CANVAS_MA
82
84
  canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/README.md,sha256=3QKoJQq3YmdplGnDOBMsLCJ3Ya1_aKjoz-QiWc4QfjA,291
83
85
  canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/protocols/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
84
86
  canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/protocols/my_protocol.py,sha256=fKLLcOIwvSWenW8-7tr8VqnF4Iox_5wU9V-Qw9UySsA,2381
85
- canvas_cli/tests.py,sha256=kY3eeDL-EIlNCeeTponVeqz9Zb5S12imUUCSzEXuYFw,6527
86
87
  canvas_cli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
87
88
  canvas_cli/utils/context/__init__.py,sha256=HhYvI-hydP0mV18nJiU7uo5gk0yN7EYNgouxieoGDOE,102
88
89
  canvas_cli/utils/context/context.py,sha256=wk4TxlslF52uD9nXcEZ1eY8L1rcEHk7k-6YBVwaWnVY,5191
@@ -113,6 +114,7 @@ canvas_sdk/__init__.py,sha256=d7V1Qsp4hSqp8opmvqp-0J33uibArUjwENMfzSDAdZg,102
113
114
  canvas_sdk/base.py,sha256=aaWilIx70lsAE76x7QaJolyIvcFnXDGPw1UMv9t0FDw,2035
114
115
  canvas_sdk/commands/__init__.py,sha256=cNQy1Y_Ji3cPush2hbaoXC3pbIFmobv_Vm-UG0w0hlc,2696
115
116
  canvas_sdk/commands/base.py,sha256=ScfOo4QXTInhTf6OGLT5dEu81yhZAcgVT8qxwIKWbow,5155
117
+ canvas_sdk/commands/constants.py,sha256=RqnokmQns-v0QErE1tCxFqFnDxsbRhigRMeTPqLTvz0,360
116
118
  canvas_sdk/commands/commands/allergy.py,sha256=_S0tY43sErRlTeUGCC6Thb8gMSD_JiOmRSFdLaDlr48,1296
117
119
  canvas_sdk/commands/commands/assess.py,sha256=IjsvkhzKinRaykMRzbnk6G4JAkzDhyqGG6ZvjDhNAuE,1723
118
120
  canvas_sdk/commands/commands/close_goal.py,sha256=RTmi8fGPEEag8YSrlsPn7sy9Y-WymVuU8IWpXRKgNjg,792
@@ -140,30 +142,29 @@ canvas_sdk/commands/commands/task.py,sha256=p-WskHqj_q8TqllarEw73AIW6n2J7uzyIso9
140
142
  canvas_sdk/commands/commands/update_diagnosis.py,sha256=fqPcKBaiRemo8Qa4H4g6ZvKM2YczVlj9FxpBGjs8J9Q,824
141
143
  canvas_sdk/commands/commands/update_goal.py,sha256=ddvxlDnoTm5k-G5aJ-pW2fMIC-s5K8jYia9tISXsxAk,1580
142
144
  canvas_sdk/commands/commands/vitals.py,sha256=hOuDXbNwqJbsBAvvkpjrXWqssrdQPxEfCox1NlI0h4A,3127
143
- canvas_sdk/commands/constants.py,sha256=RqnokmQns-v0QErE1tCxFqFnDxsbRhigRMeTPqLTvz0,360
145
+ canvas_sdk/commands/tests/test_utils.py,sha256=qkzcTDjyvrpXyPkm3f6KBVy-4lqJ1cOq2QSc-bPVPsU,12004
144
146
  canvas_sdk/commands/tests/protocol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
145
- canvas_sdk/commands/tests/protocol/tests.py,sha256=U7JZh1j1gjUh9rJGr4EZKxqtfbJqKHwMMyd46hcToa4,1889
147
+ canvas_sdk/commands/tests/protocol/tests.py,sha256=e0fiJUKsfi0aYSlaTdrMHybZntC7oBdaHks9PSrMuhU,2168
146
148
  canvas_sdk/commands/tests/schema/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
147
149
  canvas_sdk/commands/tests/schema/tests.py,sha256=rSu146RouMrPLv0G-E6QtgQhG2vkUCCk1M3Baa6fXBg,3701
148
- canvas_sdk/commands/tests/test_utils.py,sha256=1xW-ennv0rF91k3uCg1qUnsZpRRwQoeISM_mhc4SYwU,12169
149
150
  canvas_sdk/commands/tests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
150
151
  canvas_sdk/commands/tests/unit/tests.py,sha256=FsjNpUxkS4F89g341uuc5GkMCTp_JACTx32wxcwYcWg,9948
151
152
  canvas_sdk/effects/__init__.py,sha256=rcMI7IaVe8kDaYRngvlH5JPOL7FBSGIoLZB4UCN18d8,151
152
- canvas_sdk/effects/banner_alert/__init__.py,sha256=PcMmOjLHP_MOZiP8157JkTdcO4mZTn-kFcRylOB9AK0,209
153
- canvas_sdk/effects/banner_alert/add_banner_alert.py,sha256=W4Fv9IHGCWvVtFiH3s68JBRpUb8b3xsncGt2WfgMFc0,1681
154
- canvas_sdk/effects/banner_alert/remove_banner_alert.py,sha256=PK_LzXBQ-rOH3fX182ISyutu4W69xyGgMX-1G4g4XhY,613
155
- canvas_sdk/effects/banner_alert/tests.py,sha256=7YJfUanytqU_kaXsY2y6bEEwKhpXItOpmvXOncGsquc,9665
156
153
  canvas_sdk/effects/base.py,sha256=znIaDNkCYbzJxf4Q80hLRYUG5H73c8IMq1UCzeBcXjw,714
157
154
  canvas_sdk/effects/launch_modal.py,sha256=2yVY7r6hY5YYiBKae0rpS4Q0UgJPJtGlLuZGj68hm8s,1058
158
155
  canvas_sdk/effects/patient_chart_summary_configuration.py,sha256=_Gx7UIp4NLaSYYLLcBSOjtOD4Skqot_kt_x8zI3Ccwo,1184
156
+ canvas_sdk/effects/patient_profile_configuration.py,sha256=ZXBx_PnJP-KKRIwuDI4tS1evuC3_dPoiMz5KfD-QGKw,1304
157
+ canvas_sdk/effects/questionnaire_result.py,sha256=dXJE6la5aP5xLCnT9iMCOS6PiKu0Zz733Ku2lqa4CnM,816
158
+ canvas_sdk/effects/show_button.py,sha256=JnW9nM8S_GUXIOufs-uef3pg0HPDxSbF0l51Wh1Xxgw,715
159
+ canvas_sdk/effects/banner_alert/__init__.py,sha256=PcMmOjLHP_MOZiP8157JkTdcO4mZTn-kFcRylOB9AK0,209
160
+ canvas_sdk/effects/banner_alert/add_banner_alert.py,sha256=W4Fv9IHGCWvVtFiH3s68JBRpUb8b3xsncGt2WfgMFc0,1681
161
+ canvas_sdk/effects/banner_alert/remove_banner_alert.py,sha256=PK_LzXBQ-rOH3fX182ISyutu4W69xyGgMX-1G4g4XhY,613
162
+ canvas_sdk/effects/banner_alert/tests.py,sha256=DoCQ9ZfpcUBne-Le-zCCOTjzF_WHyuiay-FCex4aHJ0,9665
159
163
  canvas_sdk/effects/patient_portal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
160
164
  canvas_sdk/effects/patient_portal/intake_form_results.py,sha256=5GESYemwh7PKkmeagC8n5db6XLbtiBNV7S4b-vsUW5I,713
161
- canvas_sdk/effects/patient_profile_configuration.py,sha256=ZXBx_PnJP-KKRIwuDI4tS1evuC3_dPoiMz5KfD-QGKw,1304
162
165
  canvas_sdk/effects/protocol_card/__init__.py,sha256=AwXAARybFhVIfYwqiRThG6Ne3ARnKgvuvuGUknWpwTo,134
163
166
  canvas_sdk/effects/protocol_card/protocol_card.py,sha256=aL8lXRBm1ByU-X0xzgBepxKnUTLvmr-OC7G-C-EBjy8,2596
164
167
  canvas_sdk/effects/protocol_card/tests.py,sha256=ZiDZTjn-Z6UpEfR1S-6XDcJXlPlTPpLbg2PgvkQD8Uk,6815
165
- canvas_sdk/effects/questionnaire_result.py,sha256=dXJE6la5aP5xLCnT9iMCOS6PiKu0Zz733Ku2lqa4CnM,816
166
- canvas_sdk/effects/show_button.py,sha256=JnW9nM8S_GUXIOufs-uef3pg0HPDxSbF0l51Wh1Xxgw,715
167
168
  canvas_sdk/effects/surescripts/surescripts_messages.py,sha256=tMG5ry2mZA5pwO0uGJfYZnJuVpj5MVhS2lYT5IRsb1Y,2582
168
169
  canvas_sdk/effects/task/task.py,sha256=olG7gJN3CC1JKrGU4SdVvRq8qLduPuiNz80C_dTaY58,2837
169
170
  canvas_sdk/events/__init__.py,sha256=6Yr0Ot1yWzu_ce2ycOJJ9jdRxo0QOsSkPjNCTrjds8c,237
@@ -178,17 +179,18 @@ canvas_sdk/protocols/base.py,sha256=sbm0uOk3PPfPemqBmHh2hawE5utC6no46EmvyMN8Y7Q,
178
179
  canvas_sdk/protocols/clinical_quality_measure.py,sha256=8cU93ah9YsPecpZR1-csAbg69oFn9a8LtjHjYMMHedw,4844
179
180
  canvas_sdk/protocols/timeframe.py,sha256=SlTDhTy0TqPXKS9JZFeTVApQJDf8C-NIRLqFJltB17g,1148
180
181
  canvas_sdk/templates/__init__.py,sha256=crz8FE6yoCgwTrqosLDHM7cOiVdhWgWz0l0J--1sgmM,69
182
+ canvas_sdk/templates/utils.py,sha256=pjU9ZOAb4MHb8ui4l3f1yWRczy62JCF51rn6fWAjMKY,1645
181
183
  canvas_sdk/templates/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
182
184
  canvas_sdk/templates/tests/test_utils.py,sha256=VRahmmVwXKcp1NMLoA3BZL4cFFXzFnD-i5IUpcEeXTg,1832
183
- canvas_sdk/templates/utils.py,sha256=pjU9ZOAb4MHb8ui4l3f1yWRczy62JCF51rn6fWAjMKY,1645
184
185
  canvas_sdk/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
185
186
  canvas_sdk/utils/__init__.py,sha256=sFuhqcWvXb2-33FOuXZgWuUeC5jZL2MDoqjGoQZGwd8,60
186
187
  canvas_sdk/utils/db.py,sha256=0AO5bhu-k9OsAHpXe1RHzyDZnBGHUEzrv8-vYtTIoeA,592
187
- canvas_sdk/utils/http.py,sha256=NFEfBT1IaUJVOWFfmzkpxRbhzis7bO1Z7lX_b-LOx_U,2267
188
+ canvas_sdk/utils/http.py,sha256=x_BoQvA4_ioIjy3cjBXTZ1hWA_OXBNTySMsG6rM1t8s,2275
188
189
  canvas_sdk/utils/stats.py,sha256=sJhIW_IssUVefQN6rrUAt1P0KvVIUIYcnpZlMHLibNA,732
189
190
  canvas_sdk/utils/tests.py,sha256=0Buh_7PvDU1D081_rSJoYSJwIHMOBbL0gtGS3bSKe7s,2285
190
191
  canvas_sdk/v1/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
191
192
  canvas_sdk/v1/apps.py,sha256=z5HVdICPLYrbWM8ZXK89Xu7RWaXuKU4AFRrPAZGz0C4,151
193
+ canvas_sdk/v1/models.py,sha256=q9Sofiu9JDH5g04H8NHYIqAtBYxH4KjnwOldoKsY9Lk,206
192
194
  canvas_sdk/v1/data/__init__.py,sha256=1xQqNKh9rSwyLqT8pE8PYNOx3DTxcS9qXuU79OqjEDU,2903
193
195
  canvas_sdk/v1/data/allergy_intolerance.py,sha256=cm29fCOFiHHHVyff9kreWFUPZM--Nydw3a0FSbWh5-w,2303
194
196
  canvas_sdk/v1/data/appointment.py,sha256=rK4Vl2bE_aE73vGx8Elj-tJdDdIbQNTbl0uLtWme89Y,1900
@@ -215,10 +217,10 @@ canvas_sdk/v1/data/questionnaire.py,sha256=1Vo5QUsOLYbZiQ64QFpgY7Mj6wY8vsV8f9uPi
215
217
  canvas_sdk/v1/data/staff.py,sha256=QyFxvFAvNwgTm4gf-uPH6A8ibFE2TBhEckZpAM7gNpw,2762
216
218
  canvas_sdk/v1/data/task.py,sha256=5KNn88APPNOHEk4s1ZJRBBav8-AEQTUH039Vio_ZtAk,3471
217
219
  canvas_sdk/v1/data/user.py,sha256=XwhYTBuPHWdDc9afaZKB2AA-nHtlT3p7TOvUQo0m20Q,276
218
- canvas_sdk/v1/models.py,sha256=q9Sofiu9JDH5g04H8NHYIqAtBYxH4KjnwOldoKsY9Lk,206
219
220
  canvas_sdk/value_set/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
220
221
  canvas_sdk/value_set/custom.py,sha256=0xzGP86ioCGcsx8wwf46SXfe9IefLSGVq7Z4yrHbEUU,19709
221
222
  canvas_sdk/value_set/hcc2018.py,sha256=fNFfNzsOMJ-ALTcZaqfTU8CspZSPnZwBPDobS5DBgoY,2198461
223
+ canvas_sdk/value_set/value_set.py,sha256=KPrnlrbzzbspqmdFtPnt_raWscXiKiyTOMcPfVavt2w,3929
222
224
  canvas_sdk/value_set/tests/test_value_sets.py,sha256=ZoDdR6npR9L8h7qGdQj3B7lRghHI3PBV3klRDYV7wak,3116
223
225
  canvas_sdk/value_set/v2022/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
224
226
  canvas_sdk/value_set/v2022/adverse_event.py,sha256=8zsNFBsLDRY0mrPomzogD-W15xyo2CFFkiEcT2ruGCg,1032
@@ -237,7 +239,6 @@ canvas_sdk/value_set/v2022/medication.py,sha256=L1yNzrwD2nUox8F6x8RSKhmFsn9VcFbF
237
239
  canvas_sdk/value_set/v2022/physical_exam.py,sha256=C5PfLR4YB6pB_0wIy6Wfocyso5a9wuwGoKj84k_jR-U,7645
238
240
  canvas_sdk/value_set/v2022/procedure.py,sha256=CxbGfQc_YkQa1JpwZq90uSLaSSD4k-gPWmVNz3ard9E,367807
239
241
  canvas_sdk/value_set/v2022/symptom.py,sha256=4-ouetYI8soAAZy9Gq8AXBiqTfJjdgfZgWZPhBE-Ing,9384
240
- canvas_sdk/value_set/value_set.py,sha256=KPrnlrbzzbspqmdFtPnt_raWscXiKiyTOMcPfVavt2w,3929
241
242
  canvas_sdk/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
242
243
  logger/__init__.py,sha256=9o2iRCjzFEhfULgXvrrECRFK-4IslWJTqKKjTCEUbq8,61
243
244
  logger/logger.py,sha256=axf7UffBJtETjwDCtmi1IaaJKsvcFj8zaLfouGsq68A,1847
@@ -245,11 +246,14 @@ plugin_runner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
245
246
  plugin_runner/authentication.py,sha256=SDPso2AogtLAV_H0LuMDp99IMZuF3oTq-Q_AXAvJ8uc,1116
246
247
  plugin_runner/aws_headers.py,sha256=DenX_nAMVhXMJZw88PLZbqJsi5_XriNtr3jE-eJqHY4,2773
247
248
  plugin_runner/exceptions.py,sha256=YnRZiQVzbU3HrVlmEXLje_np99009YnhTRVHHyBCtqc,433
248
- plugin_runner/plugin_installer.py,sha256=zBjjWStccT4_H60m594qE_qIbxGrL0ecbfAboTa8Zfk,7694
249
- plugin_runner/plugin_runner.py,sha256=AnKjkKcTATVtZwohkDdnkzZHgyau3AMoFix56qRxfuc,14629
250
- plugin_runner/plugin_synchronizer.py,sha256=GcY2oKwJc8beDoAJwo-0MkY4rxNzoF-Bk1bF9MOtO6U,2851
249
+ plugin_runner/plugin_installer.py,sha256=0gHMpI4CDkWljbU7HLhbPjl2E_oHxDZMlmPtTqCyS9o,7748
250
+ plugin_runner/plugin_runner.py,sha256=h_FHStJrvRiG9DSf8uXHKFb4aby2wBzLAe10Ov4xnas,16437
251
251
  plugin_runner/sandbox.py,sha256=SdTfPWzs5mcMmQ5J9ESpTk5c3e1nQG8F4N0pKPuK48M,14499
252
252
  plugin_runner/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
253
+ plugin_runner/tests/test_application.py,sha256=e1R2YagMRD96gZALx-Zra-e-sR3SiP7cIpI6pheZnUc,2427
254
+ plugin_runner/tests/test_plugin_installer.py,sha256=i3ELG5cRfaG7ETLwk8aJhahgJuPfKjIrXL250t2wpzI,4434
255
+ plugin_runner/tests/test_plugin_runner.py,sha256=D2NyTGogu1FpiFo0MKZXcJRWFh4Ix6uznYnthdXXzk4,11632
256
+ plugin_runner/tests/test_sandbox.py,sha256=fDIkRBzoVWClvIMOI37FvJ1TRZ5mAztt88YZ0OPqsA8,4231
253
257
  plugin_runner/tests/data/plugins/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
254
258
  plugin_runner/tests/fixtures/plugins/example_plugin/CANVAS_MANIFEST.json,sha256=J9T_E5vqUX4HITHbFsd6JQpw3YvMS4wR_lhI5JL2KMk,749
255
259
  plugin_runner/tests/fixtures/plugins/example_plugin/README.md,sha256=t9pKwFf8iQPASqdXwfkA5JXkAr8KcSDX6AeW3CMiKVY,246
@@ -305,14 +309,13 @@ plugin_runner/tests/fixtures/plugins/test_render_template/README.md,sha256=EEt-t
305
309
  plugin_runner/tests/fixtures/plugins/test_render_template/protocols/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
306
310
  plugin_runner/tests/fixtures/plugins/test_render_template/protocols/my_protocol.py,sha256=uQLaxFzb4mIbmcxbIoDyz2USuWP_lW0SMI_JDaoGEPg,1521
307
311
  plugin_runner/tests/fixtures/plugins/test_render_template/templates/template.html,sha256=IWI5mZBGOPlI7AH4dT7ebxa1Q9kgSpRzC45qv3CaH7Q,132
308
- plugin_runner/tests/test_application.py,sha256=e1R2YagMRD96gZALx-Zra-e-sR3SiP7cIpI6pheZnUc,2427
309
- plugin_runner/tests/test_plugin_installer.py,sha256=i3ELG5cRfaG7ETLwk8aJhahgJuPfKjIrXL250t2wpzI,4434
310
- plugin_runner/tests/test_plugin_runner.py,sha256=qP80cID3Tm-XmDwRkWGHB5HfB5VCkXD-yqjSNMedXn4,10536
311
- plugin_runner/tests/test_sandbox.py,sha256=fDIkRBzoVWClvIMOI37FvJ1TRZ5mAztt88YZ0OPqsA8,4231
312
+ protobufs/canvas_generated/messages/effects.proto,sha256=72L5I4pnPnsNWzvZjSVzpYHlcwdyrFk1N7AGwcL0Zlc,6534
313
+ protobufs/canvas_generated/messages/events.proto,sha256=ymaaxJwXG71nbbVmEQwqAyBtZFFljUTY8sjQ-ju7GdA,45727
314
+ protobufs/canvas_generated/messages/plugins.proto,sha256=oNainUPWFYQjgCX7bJEPI9_VnHC5VZduzOqgR4Q7dNM,109
315
+ protobufs/canvas_generated/services/plugin_runner.proto,sha256=doadBKn5k4xAtOgR-q_pEvW4yzxpUaHNOowMG6CL5GY,304
312
316
  pubsub/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
313
317
  pubsub/pubsub.py,sha256=pyTW0JU8mtaqiAV6g6xjZwel1CVy2EonPMU-_vkmhUM,1044
314
- settings.py,sha256=ybItVrMEw7cAa7XO4mw9yMHRNANaX4bzZaaYkNOElCI,2573
315
- canvas-0.16.0.dist-info/METADATA,sha256=Us42iLPnQtsVyk0ClTSQZW_mCo47Tqns9FAjQVQKmiI,4703
316
- canvas-0.16.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
317
- canvas-0.16.0.dist-info/entry_points.txt,sha256=VSmSo1IZ3aEfL7enmLmlWSraS_IIkoXNVeyXzgRxFiY,46
318
- canvas-0.16.0.dist-info/RECORD,,
318
+ canvas-0.17.0.dist-info/METADATA,sha256=UCQcCBA9TGVqFz4CR5ypshNjiPVNVPscthbRtDgJuEs,4375
319
+ canvas-0.17.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
320
+ canvas-0.17.0.dist-info/entry_points.txt,sha256=0Vs_9GmTVUNniH6eDBlRPgofmADMV4BES6Ao26M4AbM,47
321
+ canvas-0.17.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.0
2
+ Generator: hatchling 1.27.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ canvas = canvas_cli.main:app
@@ -40,7 +40,7 @@ def validate_package(package: Path) -> Path:
40
40
 
41
41
 
42
42
  def _build_package(package: Path) -> Path:
43
- """Runs `poetry build` on `package` and returns the built archive, ignoring symlinks, hidden folders, and hidden files."""
43
+ """Compresses `package` and returns the built archive, ignoring symlinks, hidden folders, and hidden files."""
44
44
  package = package.resolve()
45
45
 
46
46
  if not package.exists() or not package.is_dir():
@@ -1,8 +1,10 @@
1
1
  from collections.abc import Generator
2
2
  from datetime import datetime
3
+ from typing import cast
3
4
 
4
5
  import pytest
5
6
 
7
+ import settings
6
8
  from canvas_sdk.commands.tests.test_utils import (
7
9
  COMMANDS,
8
10
  MaskedValue,
@@ -12,6 +14,7 @@ from canvas_sdk.commands.tests.test_utils import (
12
14
  get_token,
13
15
  install_plugin,
14
16
  trigger_plugin_event,
17
+ wait_for_log,
15
18
  write_protocol_code,
16
19
  )
17
20
 
@@ -40,10 +43,18 @@ def write_and_install_protocol_and_clean_up(
40
43
  ) -> Generator[None, None, None]:
41
44
  """Write the protocol code, install the plugin, and clean up after the test."""
42
45
  write_protocol_code(new_note["externallyExposableId"], plugin_name, COMMANDS)
46
+ message_received_event, thread, ws = wait_for_log(
47
+ cast(str, settings.INTEGRATION_TEST_URL),
48
+ token.value,
49
+ f"Loading plugin '{plugin_name}",
50
+ )
43
51
  install_plugin(plugin_name, token)
52
+ message_received_event.wait(timeout=5.0)
44
53
 
45
54
  yield
46
55
 
56
+ ws.close()
57
+ thread.join()
47
58
  clean_up_files_and_plugins(plugin_name, token)
48
59
 
49
60
 
@@ -57,8 +68,7 @@ def test_protocol_that_inserts_every_command(
57
68
  commands_in_body = get_original_note_body_commands(new_note["id"], token)
58
69
 
59
70
  # TODO: Temporary workaround to ignore the updateGoal command until the integration test instance is fixed.
60
- command_keys = [c.Meta.key for c in COMMANDS if c.Meta.key != "updateGoal"]
61
-
71
+ command_keys = [c.Meta.key for c in COMMANDS]
62
72
  assert len(command_keys) == len(commands_in_body)
63
73
  for i, command_key in enumerate(command_keys):
64
74
  assert commands_in_body[i] == command_key
@@ -190,11 +190,6 @@ class Protocol(BaseProtocol):
190
190
  def install_plugin(plugin_name: str, token: MaskedValue) -> None:
191
191
  """Install a plugin."""
192
192
  with open(_build_package(Path(f"./custom-plugins/{plugin_name}")), "rb") as package:
193
- message_received_event = wait_for_log(
194
- cast(str, settings.INTEGRATION_TEST_URL),
195
- token.value,
196
- f"Loading plugin '{plugin_name}",
197
- )
198
193
  response = requests.post(
199
194
  plugin_url(cast(str, settings.INTEGRATION_TEST_URL)),
200
195
  data={"is_enabled": True},
@@ -203,8 +198,6 @@ def install_plugin(plugin_name: str, token: MaskedValue) -> None:
203
198
  )
204
199
  response.raise_for_status()
205
200
 
206
- message_received_event.wait(timeout=5.0)
207
-
208
201
 
209
202
  def trigger_plugin_event(token: MaskedValue) -> None:
210
203
  """Trigger a plugin event."""
@@ -328,7 +321,9 @@ def get_token() -> MaskedValue:
328
321
  return MaskedValue(response.json()["access_token"])
329
322
 
330
323
 
331
- def wait_for_log(host: str, token: str, message: str) -> threading.Event:
324
+ def wait_for_log(
325
+ host: str, token: str, message: str
326
+ ) -> tuple[threading.Event, threading.Thread, websocket.WebSocketApp]:
332
327
  """Wait for a specific log message."""
333
328
  hostname = cast(str, urlparse(host).hostname)
334
329
  instance = hostname.removesuffix(".canvasmedical.com")
@@ -362,4 +357,4 @@ def wait_for_log(host: str, token: str, message: str) -> threading.Event:
362
357
 
363
358
  connected_event.wait(timeout=5.0)
364
359
 
365
- return message_received_event
360
+ return message_received_event, thread, ws
@@ -92,10 +92,10 @@ class Protocol(BaseProtocol):
92
92
  protocol.write(protocol_code)
93
93
 
94
94
  with open(_build_package(Path(f"./custom-plugins/{plugin_name}")), "rb") as package:
95
- message_received_event = wait_for_log(
95
+ message_received_event, thread, ws = wait_for_log(
96
96
  settings.INTEGRATION_TEST_URL,
97
97
  token.value,
98
- f"Loading plugin '{plugin_name}:{plugin_name}.protocols.my_protocol:Protocol'",
98
+ f"Loading plugin '{plugin_name}",
99
99
  )
100
100
 
101
101
  # install the plugin
@@ -111,6 +111,9 @@ class Protocol(BaseProtocol):
111
111
 
112
112
  yield
113
113
 
114
+ ws.close()
115
+ thread.join()
116
+
114
117
  # clean up
115
118
  if Path(f"./custom-plugins/{plugin_name}").exists():
116
119
  shutil.rmtree(Path(f"./custom-plugins/{plugin_name}"))
canvas_sdk/utils/http.py CHANGED
@@ -26,7 +26,7 @@ class Http:
26
26
  result = fn(self, *args, **kwargs)
27
27
  end_time = time.time()
28
28
  timing = int((end_time - start_time) * 1000)
29
- self.statsd_client.timing(f"http_{fn.__name__}", timing)
29
+ self.statsd_client.timing(f"plugins.http_{fn.__name__}", timing)
30
30
  return result
31
31
 
32
32
  return cast(F, wrapper)
@@ -14,9 +14,17 @@ import requests
14
14
  from psycopg import Connection
15
15
  from psycopg.rows import dict_row
16
16
 
17
- import settings
18
17
  from plugin_runner.aws_headers import aws_sig_v4_headers
19
18
  from plugin_runner.exceptions import InvalidPluginFormat, PluginInstallationError
19
+ from settings import (
20
+ AWS_ACCESS_KEY_ID,
21
+ AWS_REGION,
22
+ AWS_SECRET_ACCESS_KEY,
23
+ CUSTOMER_IDENTIFIER,
24
+ MEDIA_S3_BUCKET_NAME,
25
+ PLUGIN_DIRECTORY,
26
+ SECRETS_FILE_NAME,
27
+ )
20
28
 
21
29
  # Plugin "packages" include this prefix in the database record for the plugin and the S3 bucket key.
22
30
  UPLOAD_TO_PREFIX = "plugins"
@@ -100,19 +108,19 @@ def _extract_rows_to_dict(rows: list) -> dict[str, PluginAttributes]:
100
108
  def download_plugin(plugin_package: str) -> Generator[Path, None, None]:
101
109
  """Download the plugin package from the S3 bucket."""
102
110
  method = "GET"
103
- host = f"s3-{settings.AWS_REGION}.amazonaws.com"
104
- bucket = settings.MEDIA_S3_BUCKET_NAME
105
- customer_identifier = settings.CUSTOMER_IDENTIFIER
111
+ host = f"s3-{AWS_REGION}.amazonaws.com"
112
+ bucket = MEDIA_S3_BUCKET_NAME
113
+ customer_identifier = CUSTOMER_IDENTIFIER
106
114
  path = f"/{bucket}/{customer_identifier}/{plugin_package}"
107
115
  payload = b"This is required for the AWS headers because it is part of the signature"
108
116
  pre_auth_headers: dict[str, str] = {}
109
117
  query: dict[str, str] = {}
110
118
  headers = aws_sig_v4_headers(
111
- settings.AWS_ACCESS_KEY_ID,
112
- settings.AWS_SECRET_ACCESS_KEY,
119
+ AWS_ACCESS_KEY_ID,
120
+ AWS_SECRET_ACCESS_KEY,
113
121
  pre_auth_headers,
114
122
  "s3",
115
- settings.AWS_REGION,
123
+ AWS_REGION,
116
124
  host,
117
125
  method,
118
126
  path,
@@ -135,7 +143,7 @@ def install_plugin(plugin_name: str, attributes: PluginAttributes) -> None:
135
143
  try:
136
144
  print(f"Installing plugin '{plugin_name}'")
137
145
 
138
- plugin_installation_path = Path(settings.PLUGIN_DIRECTORY) / plugin_name
146
+ plugin_installation_path = Path(PLUGIN_DIRECTORY) / plugin_name
139
147
 
140
148
  # if plugin exists, first uninstall it
141
149
  if plugin_installation_path.exists():
@@ -175,7 +183,7 @@ def install_plugin_secrets(plugin_name: str, secrets: dict[str, str]) -> None:
175
183
  """Write the plugin's secrets to disk in the package's directory."""
176
184
  print(f"Writing plugin secrets for '{plugin_name}'")
177
185
 
178
- secrets_path = Path(settings.PLUGIN_DIRECTORY) / plugin_name / settings.SECRETS_FILE_NAME
186
+ secrets_path = Path(PLUGIN_DIRECTORY) / plugin_name / SECRETS_FILE_NAME
179
187
 
180
188
  # Did the plugin ship a secrets.json? TOO BAD, IT'S GONE NOW.
181
189
  if Path(secrets_path).exists():
@@ -199,7 +207,7 @@ def disable_plugin(plugin_name: str) -> None:
199
207
 
200
208
  def uninstall_plugin(plugin_name: str) -> None:
201
209
  """Remove the plugin from the filesystem."""
202
- plugin_path = Path(settings.PLUGIN_DIRECTORY) / plugin_name
210
+ plugin_path = Path(PLUGIN_DIRECTORY) / plugin_name
203
211
 
204
212
  if plugin_path.exists():
205
213
  shutil.rmtree(plugin_path)
@@ -207,10 +215,10 @@ def uninstall_plugin(plugin_name: str) -> None:
207
215
 
208
216
  def install_plugins() -> None:
209
217
  """Install all enabled plugins."""
210
- if Path(settings.PLUGIN_DIRECTORY).exists():
211
- shutil.rmtree(settings.PLUGIN_DIRECTORY)
218
+ if Path(PLUGIN_DIRECTORY).exists():
219
+ shutil.rmtree(PLUGIN_DIRECTORY)
212
220
 
213
- os.mkdir(settings.PLUGIN_DIRECTORY)
221
+ os.mkdir(PLUGIN_DIRECTORY)
214
222
 
215
223
  for plugin_name, attributes in enabled_plugins().items():
216
224
  try:
@@ -2,17 +2,17 @@ import asyncio
2
2
  import json
3
3
  import os
4
4
  import pathlib
5
+ import pickle
5
6
  import pkgutil
6
- import signal
7
7
  import sys
8
8
  import time
9
9
  import traceback
10
10
  from collections import defaultdict
11
11
  from collections.abc import AsyncGenerator
12
- from types import FrameType
13
12
  from typing import Any, TypedDict
14
13
 
15
14
  import grpc
15
+ import redis.asyncio as redis
16
16
  import statsd
17
17
 
18
18
  from canvas_generated.messages.effects_pb2 import EffectType
@@ -30,9 +30,15 @@ from canvas_sdk.protocols import ClinicalQualityMeasure
30
30
  from canvas_sdk.utils.stats import get_duration_ms, tags_to_line_protocol
31
31
  from logger import log
32
32
  from plugin_runner.authentication import token_for_plugin
33
- from plugin_runner.plugin_synchronizer import publish_message
33
+ from plugin_runner.plugin_installer import install_plugins
34
34
  from plugin_runner.sandbox import Sandbox
35
- from settings import MANIFEST_FILE_NAME, PLUGIN_DIRECTORY, SECRETS_FILE_NAME
35
+ from settings import (
36
+ CHANNEL_NAME,
37
+ MANIFEST_FILE_NAME,
38
+ PLUGIN_DIRECTORY,
39
+ REDIS_ENDPOINT,
40
+ SECRETS_FILE_NAME,
41
+ )
36
42
 
37
43
  # when we import plugins we'll use the module name directly so we need to add the plugin
38
44
  # directory to the path
@@ -192,14 +198,50 @@ class PluginRunner(PluginRunnerServicer):
192
198
  self, request: ReloadPluginsRequest, context: Any
193
199
  ) -> AsyncGenerator[ReloadPluginsResponse, None]:
194
200
  """This is invoked when we need to reload plugins."""
201
+ log.info("Reloading plugins...")
195
202
  try:
196
- publish_message({"action": "restart"})
203
+ await publish_message(message={"action": "reload"})
197
204
  except ImportError:
198
205
  yield ReloadPluginsResponse(success=False)
199
206
  else:
200
207
  yield ReloadPluginsResponse(success=True)
201
208
 
202
209
 
210
+ async def synchronize_plugins(max_iterations: None | int = None) -> None:
211
+ """Listen for messages on the pubsub channel that will indicate it is necessary to reinstall and reload plugins."""
212
+ client, pubsub = get_client()
213
+ await pubsub.psubscribe(CHANNEL_NAME)
214
+ log.info("Listening for messages on pubsub channel")
215
+ iterations: int = 0
216
+ while (
217
+ max_iterations is None or iterations < max_iterations
218
+ ): # max_iterations == -1 means infinite iterations
219
+ iterations += 1
220
+ message = await pubsub.get_message(ignore_subscribe_messages=True, timeout=None)
221
+ if message is not None:
222
+ log.info("Received message from pubsub channel")
223
+
224
+ message_type = message.get("type", "")
225
+
226
+ if message_type != "pmessage":
227
+ continue
228
+
229
+ data = pickle.loads(message.get("data", pickle.dumps({})))
230
+
231
+ if "action" not in data:
232
+ continue
233
+
234
+ if data["action"] == "reload":
235
+ try:
236
+ log.info(
237
+ "plugin-synchronizer: installing and reloading plugins after receiving command"
238
+ )
239
+ install_plugins()
240
+ load_plugins()
241
+ except Exception as e:
242
+ print("plugin-synchronizer: `install_plugins` failed:", e)
243
+
244
+
203
245
  def validate_effects(effects: list[Effect]) -> list[Effect]:
204
246
  """Validates the effects based on predefined rules.
205
247
  Keeps only the first AUTOCOMPLETE_SEARCH_RESULTS effect and preserve all non-search-related effects.
@@ -237,12 +279,6 @@ def apply_effects_to_context(effects: list[Effect], event: Event) -> Event:
237
279
  return event
238
280
 
239
281
 
240
- def handle_hup_cb(_signum: int, _frame: FrameType | None) -> None:
241
- """handle_hup_cb."""
242
- log.info("Received SIGHUP, reloading plugins...")
243
- load_plugins()
244
-
245
-
246
282
  def find_modules(base_path: pathlib.Path, prefix: str | None = None) -> list[str]:
247
283
  """Find all modules in the specified package path."""
248
284
  modules: list[str] = []
@@ -273,6 +309,22 @@ def sandbox_from_module(base_path: pathlib.Path, module_name: str) -> Any:
273
309
  return sandbox.execute()
274
310
 
275
311
 
312
+ async def publish_message(message: dict) -> None:
313
+ """Publish a message to the pubsub channel."""
314
+ log.info("Publishing message to pubsub channel")
315
+ client, _ = get_client()
316
+
317
+ await client.publish(CHANNEL_NAME, pickle.dumps(message))
318
+
319
+
320
+ def get_client() -> tuple[redis.Redis, redis.client.PubSub]:
321
+ """Return an async Redis client and pubsub object."""
322
+ client = redis.Redis.from_url(REDIS_ENDPOINT)
323
+ pubsub = client.pubsub()
324
+
325
+ return client, pubsub
326
+
327
+
276
328
  def load_or_reload_plugin(path: pathlib.Path) -> None:
277
329
  """Given a path, load or reload a plugin."""
278
330
  log.info(f"Loading {path}")
@@ -415,6 +467,7 @@ async def serve(specified_plugin_paths: list[str] | None = None) -> None:
415
467
 
416
468
  log.info(f"Starting server, listening on port {port}")
417
469
 
470
+ install_plugins()
418
471
  load_plugins(specified_plugin_paths)
419
472
 
420
473
  await server.start()
@@ -434,10 +487,10 @@ def run_server(specified_plugin_paths: list[str] | None = None) -> None:
434
487
 
435
488
  asyncio.set_event_loop(loop)
436
489
 
437
- signal.signal(signal.SIGHUP, handle_hup_cb)
438
-
439
490
  try:
440
- loop.run_until_complete(serve(specified_plugin_paths))
491
+ loop.run_until_complete(
492
+ asyncio.gather(serve(specified_plugin_paths), synchronize_plugins())
493
+ )
441
494
  except KeyboardInterrupt:
442
495
  pass
443
496
  finally: