canvas 0.1.12__py3-none-any.whl → 0.1.14__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of canvas might be problematic. Click here for more details.
- {canvas-0.1.12.dist-info → canvas-0.1.14.dist-info}/METADATA +42 -1
- canvas-0.1.14.dist-info/RECORD +90 -0
- canvas_cli/apps/plugin/__init__.py +3 -1
- canvas_cli/apps/plugin/plugin.py +74 -0
- canvas_cli/main.py +5 -3
- canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/protocols/my_protocol.py +3 -0
- canvas_generated/messages/effects_pb2.py +28 -0
- canvas_generated/messages/effects_pb2.pyi +147 -0
- canvas_generated/messages/effects_pb2_grpc.py +4 -0
- canvas_generated/messages/events_pb2.py +31 -0
- canvas_generated/messages/events_pb2.pyi +445 -0
- canvas_generated/messages/events_pb2_grpc.py +4 -0
- canvas_generated/messages/plugins_pb2.py +28 -0
- canvas_generated/messages/plugins_pb2.pyi +15 -0
- canvas_generated/messages/plugins_pb2_grpc.py +4 -0
- canvas_generated/services/plugin_runner_pb2.py +28 -0
- canvas_generated/services/plugin_runner_pb2.pyi +6 -0
- canvas_generated/services/plugin_runner_pb2_grpc.py +100 -0
- canvas_sdk/base.py +45 -0
- canvas_sdk/commands/base.py +61 -41
- canvas_sdk/commands/commands/assess.py +6 -2
- canvas_sdk/commands/commands/diagnose.py +4 -14
- canvas_sdk/commands/commands/goal.py +3 -2
- canvas_sdk/commands/commands/history_present_illness.py +2 -1
- canvas_sdk/commands/commands/medication_statement.py +6 -2
- canvas_sdk/commands/commands/plan.py +2 -1
- canvas_sdk/commands/commands/prescribe.py +24 -11
- canvas_sdk/commands/commands/questionnaire.py +6 -2
- canvas_sdk/commands/commands/reason_for_visit.py +13 -6
- canvas_sdk/commands/commands/stop_medication.py +6 -2
- canvas_sdk/commands/commands/update_goal.py +4 -1
- canvas_sdk/commands/tests/test_utils.py +31 -64
- canvas_sdk/commands/tests/tests.py +116 -65
- canvas_sdk/data/__init__.py +1 -0
- canvas_sdk/data/base.py +22 -0
- canvas_sdk/data/patient.py +6 -0
- canvas_sdk/data/staff.py +6 -0
- canvas_sdk/data/task.py +60 -0
- canvas_sdk/effects/__init__.py +1 -1
- canvas_sdk/effects/banner_alert/__init__.py +2 -0
- canvas_sdk/effects/banner_alert/add_banner_alert.py +49 -0
- canvas_sdk/effects/banner_alert/remove_banner_alert.py +20 -0
- canvas_sdk/effects/base.py +4 -6
- canvas_sdk/events/__init__.py +1 -1
- canvas_sdk/handlers/__init__.py +1 -0
- canvas_sdk/handlers/base.py +16 -0
- canvas_sdk/handlers/cron_task.py +35 -0
- canvas_sdk/protocols/base.py +2 -11
- canvas_sdk/utils/stats.py +27 -0
- logger/__init__.py +2 -0
- logger/logger.py +48 -0
- canvas-0.1.12.dist-info/RECORD +0 -66
- canvas_sdk/effects/banner_alert/banner_alert.py +0 -37
- canvas_sdk/effects/banner_alert/constants.py +0 -19
- {canvas-0.1.12.dist-info → canvas-0.1.14.dist-info}/WHEEL +0 -0
- {canvas-0.1.12.dist-info → canvas-0.1.14.dist-info}/entry_points.txt +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import decimal
|
|
1
2
|
from datetime import datetime
|
|
2
3
|
|
|
3
4
|
import pytest
|
|
@@ -20,10 +21,10 @@ from canvas_sdk.commands import (
|
|
|
20
21
|
)
|
|
21
22
|
from canvas_sdk.commands.constants import Coding
|
|
22
23
|
from canvas_sdk.commands.tests.test_utils import (
|
|
24
|
+
MaskedValue,
|
|
23
25
|
fake,
|
|
24
26
|
get_field_type,
|
|
25
|
-
|
|
26
|
-
raises_none_error,
|
|
27
|
+
raises_none_error_for_effect_method,
|
|
27
28
|
raises_wrong_type_error,
|
|
28
29
|
)
|
|
29
30
|
|
|
@@ -57,7 +58,6 @@ from canvas_sdk.commands.tests.test_utils import (
|
|
|
57
58
|
"icd10_codes",
|
|
58
59
|
"sig",
|
|
59
60
|
"days_supply",
|
|
60
|
-
"quantity_to_dispense",
|
|
61
61
|
"type_to_dispense",
|
|
62
62
|
"refills",
|
|
63
63
|
"substitutions",
|
|
@@ -97,15 +97,11 @@ def test_command_raises_generic_error_when_kwarg_given_incorrect_type(
|
|
|
97
97
|
),
|
|
98
98
|
fields_to_test: tuple[str],
|
|
99
99
|
) -> None:
|
|
100
|
-
schema = Command.model_json_schema()
|
|
101
|
-
schema["required"].append("note_id")
|
|
102
|
-
required_fields = {k: v for k, v in schema["properties"].items() if k in schema["required"]}
|
|
103
|
-
base = {field: fake(props, Command) for field, props in required_fields.items()}
|
|
104
100
|
for field in fields_to_test:
|
|
105
|
-
raises_wrong_type_error(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
101
|
+
raises_wrong_type_error(Command, field)
|
|
102
|
+
|
|
103
|
+
for method in ["originate", "edit", "delete", "commit", "enter_in_error"]:
|
|
104
|
+
raises_none_error_for_effect_method(Command, method)
|
|
109
105
|
|
|
110
106
|
|
|
111
107
|
@pytest.mark.parametrize(
|
|
@@ -113,69 +109,101 @@ def test_command_raises_generic_error_when_kwarg_given_incorrect_type(
|
|
|
113
109
|
[
|
|
114
110
|
(
|
|
115
111
|
PlanCommand,
|
|
116
|
-
{"narrative": "yo", "user_id": 1},
|
|
117
|
-
"1 validation error for PlanCommand\n
|
|
118
|
-
{"narrative": "yo", "
|
|
112
|
+
{"narrative": "yo", "user_id": 5, "note_uuid": 1},
|
|
113
|
+
"1 validation error for PlanCommand\nnote_uuid\n Input should be a valid string [type=string_type",
|
|
114
|
+
{"narrative": "yo", "note_uuid": "00000000-0000-0000-0000-000000000000", "user_id": 1},
|
|
119
115
|
),
|
|
120
116
|
(
|
|
121
117
|
PlanCommand,
|
|
122
|
-
{"narrative": "yo", "user_id":
|
|
123
|
-
"1 validation error for PlanCommand\n
|
|
124
|
-
{"narrative": "yo", "
|
|
118
|
+
{"narrative": "yo", "user_id": 5, "note_uuid": "5", "command_uuid": 5},
|
|
119
|
+
"1 validation error for PlanCommand\ncommand_uuid\n Input should be a valid string [type=string_type",
|
|
120
|
+
{"narrative": "yo", "user_id": 5, "note_uuid": "5", "command_uuid": "5"},
|
|
125
121
|
),
|
|
126
122
|
(
|
|
127
123
|
PlanCommand,
|
|
128
|
-
{"narrative": "yo", "
|
|
129
|
-
"1 validation error for PlanCommand\
|
|
130
|
-
{"narrative": "yo", "
|
|
124
|
+
{"narrative": "yo", "note_uuid": "5", "command_uuid": "4", "user_id": "5"},
|
|
125
|
+
"1 validation error for PlanCommand\nuser_id\n Input should be a valid integer [type=int_type",
|
|
126
|
+
{"narrative": "yo", "note_uuid": "5", "command_uuid": "4", "user_id": 5},
|
|
131
127
|
),
|
|
132
128
|
(
|
|
133
129
|
ReasonForVisitCommand,
|
|
134
|
-
{"
|
|
135
|
-
"1 validation error for ReasonForVisitCommand\n
|
|
136
|
-
{
|
|
130
|
+
{"note_uuid": "00000000-0000-0000-0000-000000000000", "user_id": 1, "structured": True},
|
|
131
|
+
"1 validation error for ReasonForVisitCommand\n Structured RFV should have a coding",
|
|
132
|
+
{
|
|
133
|
+
"note_uuid": "00000000-0000-0000-0000-000000000000",
|
|
134
|
+
"user_id": 1,
|
|
135
|
+
"structured": False,
|
|
136
|
+
},
|
|
137
137
|
),
|
|
138
138
|
(
|
|
139
139
|
ReasonForVisitCommand,
|
|
140
|
-
{
|
|
140
|
+
{
|
|
141
|
+
"note_uuid": "00000000-0000-0000-0000-000000000000",
|
|
142
|
+
"user_id": 1,
|
|
143
|
+
"coding": {"code": "x"},
|
|
144
|
+
},
|
|
141
145
|
"1 validation error for ReasonForVisitCommand\ncoding.system\n Field required [type=missing",
|
|
142
|
-
{"
|
|
146
|
+
{"note_uuid": "00000000-0000-0000-0000-000000000000", "user_id": 1},
|
|
143
147
|
),
|
|
144
148
|
(
|
|
145
149
|
ReasonForVisitCommand,
|
|
146
|
-
{
|
|
150
|
+
{
|
|
151
|
+
"note_uuid": "00000000-0000-0000-0000-000000000000",
|
|
152
|
+
"user_id": 1,
|
|
153
|
+
"coding": {"code": 1, "system": "y"},
|
|
154
|
+
},
|
|
147
155
|
"1 validation error for ReasonForVisitCommand\ncoding.code\n Input should be a valid string [type=string_type",
|
|
148
|
-
{"
|
|
156
|
+
{"note_uuid": "00000000-0000-0000-0000-000000000000", "user_id": 1},
|
|
149
157
|
),
|
|
150
158
|
(
|
|
151
159
|
ReasonForVisitCommand,
|
|
152
|
-
{
|
|
160
|
+
{
|
|
161
|
+
"note_uuid": "00000000-0000-0000-0000-000000000000",
|
|
162
|
+
"user_id": 1,
|
|
163
|
+
"coding": {"code": None, "system": "y"},
|
|
164
|
+
},
|
|
153
165
|
"1 validation error for ReasonForVisitCommand\ncoding.code\n Input should be a valid string [type=string_type",
|
|
154
|
-
{"
|
|
166
|
+
{"note_uuid": "00000000-0000-0000-0000-000000000000", "user_id": 1},
|
|
155
167
|
),
|
|
156
168
|
(
|
|
157
169
|
ReasonForVisitCommand,
|
|
158
|
-
{
|
|
170
|
+
{
|
|
171
|
+
"note_uuid": "00000000-0000-0000-0000-000000000000",
|
|
172
|
+
"user_id": 1,
|
|
173
|
+
"coding": {"system": "y"},
|
|
174
|
+
},
|
|
159
175
|
"1 validation error for ReasonForVisitCommand\ncoding.code\n Field required [type=missing",
|
|
160
|
-
{"
|
|
176
|
+
{"note_uuid": "00000000-0000-0000-0000-000000000000", "user_id": 1},
|
|
161
177
|
),
|
|
162
178
|
(
|
|
163
179
|
ReasonForVisitCommand,
|
|
164
|
-
{
|
|
180
|
+
{
|
|
181
|
+
"note_uuid": "00000000-0000-0000-0000-000000000000",
|
|
182
|
+
"user_id": 1,
|
|
183
|
+
"coding": {"code": "x", "system": 1},
|
|
184
|
+
},
|
|
165
185
|
"1 validation error for ReasonForVisitCommand\ncoding.system\n Input should be a valid string [type=string_type",
|
|
166
|
-
{"
|
|
186
|
+
{"note_uuid": "00000000-0000-0000-0000-000000000000", "user_id": 1},
|
|
167
187
|
),
|
|
168
188
|
(
|
|
169
189
|
ReasonForVisitCommand,
|
|
170
|
-
{
|
|
190
|
+
{
|
|
191
|
+
"note_uuid": "00000000-0000-0000-0000-000000000000",
|
|
192
|
+
"user_id": 1,
|
|
193
|
+
"coding": {"code": "x", "system": None},
|
|
194
|
+
},
|
|
171
195
|
"1 validation error for ReasonForVisitCommand\ncoding.system\n Input should be a valid string [type=string_type",
|
|
172
|
-
{"
|
|
196
|
+
{"note_uuid": "00000000-0000-0000-0000-000000000000", "user_id": 1},
|
|
173
197
|
),
|
|
174
198
|
(
|
|
175
199
|
ReasonForVisitCommand,
|
|
176
|
-
{
|
|
200
|
+
{
|
|
201
|
+
"note_uuid": "00000000-0000-0000-0000-000000000000",
|
|
202
|
+
"user_id": 1,
|
|
203
|
+
"coding": {"code": "x", "system": "y", "display": 1},
|
|
204
|
+
},
|
|
177
205
|
"1 validation error for ReasonForVisitCommand\ncoding.display\n Input should be a valid string [type=string_type",
|
|
178
|
-
{"
|
|
206
|
+
{"note_uuid": "00000000-0000-0000-0000-000000000000", "user_id": 1},
|
|
179
207
|
),
|
|
180
208
|
],
|
|
181
209
|
)
|
|
@@ -186,7 +214,9 @@ def test_command_raises_specific_error_when_kwarg_given_incorrect_type(
|
|
|
186
214
|
valid_kwargs: dict,
|
|
187
215
|
) -> None:
|
|
188
216
|
with pytest.raises(ValidationError) as e1:
|
|
189
|
-
Command(**err_kwargs)
|
|
217
|
+
cmd = Command(**err_kwargs)
|
|
218
|
+
cmd.originate()
|
|
219
|
+
cmd.edit()
|
|
190
220
|
assert err_msg in repr(e1.value)
|
|
191
221
|
|
|
192
222
|
cmd = Command(**valid_kwargs)
|
|
@@ -195,6 +225,8 @@ def test_command_raises_specific_error_when_kwarg_given_incorrect_type(
|
|
|
195
225
|
key, value = list(err_kwargs.items())[-1]
|
|
196
226
|
with pytest.raises(ValidationError) as e2:
|
|
197
227
|
setattr(cmd, key, value)
|
|
228
|
+
cmd.originate()
|
|
229
|
+
cmd.edit()
|
|
198
230
|
assert err_msg in repr(e2.value)
|
|
199
231
|
|
|
200
232
|
|
|
@@ -219,7 +251,7 @@ def test_command_raises_specific_error_when_kwarg_given_incorrect_type(
|
|
|
219
251
|
),
|
|
220
252
|
(HistoryOfPresentIllnessCommand, ("narrative",)),
|
|
221
253
|
(MedicationStatementCommand, ("fdb_code", "sig")),
|
|
222
|
-
(PlanCommand, ("narrative", "user_id", "command_uuid", "
|
|
254
|
+
(PlanCommand, ("narrative", "user_id", "command_uuid", "note_uuid")),
|
|
223
255
|
(
|
|
224
256
|
PrescribeCommand,
|
|
225
257
|
(
|
|
@@ -268,15 +300,12 @@ def test_command_allows_kwarg_with_correct_type(
|
|
|
268
300
|
fields_to_test: tuple[str],
|
|
269
301
|
) -> None:
|
|
270
302
|
schema = Command.model_json_schema()
|
|
271
|
-
schema["required"].append("note_id")
|
|
272
|
-
required_fields = {k: v for k, v in schema["properties"].items() if k in schema["required"]}
|
|
273
|
-
base = {field: fake(props, Command) for field, props in required_fields.items()}
|
|
274
303
|
|
|
275
304
|
for field in fields_to_test:
|
|
276
|
-
field_type = get_field_type(
|
|
305
|
+
field_type = get_field_type(schema["properties"][field])
|
|
277
306
|
|
|
278
307
|
init_field_value = fake({"type": field_type}, Command)
|
|
279
|
-
init_kwargs =
|
|
308
|
+
init_kwargs = {field: init_field_value}
|
|
280
309
|
cmd = Command(**init_kwargs)
|
|
281
310
|
assert getattr(cmd, field) == init_field_value
|
|
282
311
|
|
|
@@ -284,24 +313,37 @@ def test_command_allows_kwarg_with_correct_type(
|
|
|
284
313
|
setattr(cmd, field, updated_field_value)
|
|
285
314
|
assert getattr(cmd, field) == updated_field_value
|
|
286
315
|
|
|
316
|
+
for method in ["originate", "edit", "delete", "commit", "enter_in_error"]:
|
|
317
|
+
required_fields = {
|
|
318
|
+
k: v
|
|
319
|
+
for k, v in schema["properties"].items()
|
|
320
|
+
if k in Command()._get_effect_method_required_fields(method)
|
|
321
|
+
}
|
|
322
|
+
base = {field: fake(props, Command) for field, props in required_fields.items()}
|
|
323
|
+
cmd = Command(**base)
|
|
324
|
+
effect = getattr(cmd, method)()
|
|
325
|
+
assert effect is not None
|
|
326
|
+
|
|
287
327
|
|
|
288
328
|
@pytest.fixture(scope="session")
|
|
289
|
-
def token() ->
|
|
290
|
-
return
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
329
|
+
def token() -> MaskedValue:
|
|
330
|
+
return MaskedValue(
|
|
331
|
+
requests.post(
|
|
332
|
+
f"{settings.INTEGRATION_TEST_URL}/auth/token/",
|
|
333
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
334
|
+
data={
|
|
335
|
+
"grant_type": "client_credentials",
|
|
336
|
+
"client_id": settings.INTEGRATION_TEST_CLIENT_ID,
|
|
337
|
+
"client_secret": settings.INTEGRATION_TEST_CLIENT_SECRET,
|
|
338
|
+
},
|
|
339
|
+
).json()["access_token"]
|
|
340
|
+
)
|
|
299
341
|
|
|
300
342
|
|
|
301
343
|
@pytest.fixture
|
|
302
|
-
def
|
|
344
|
+
def note_uuid(token: MaskedValue) -> str:
|
|
303
345
|
headers = {
|
|
304
|
-
"Authorization": f"Bearer {token}",
|
|
346
|
+
"Authorization": f"Bearer {token.value}",
|
|
305
347
|
"Content-Type": "application/json",
|
|
306
348
|
"Accept": "application/json",
|
|
307
349
|
}
|
|
@@ -326,6 +368,9 @@ def command_type_map() -> dict[str, type]:
|
|
|
326
368
|
"TextField": str,
|
|
327
369
|
"ChoiceField": str,
|
|
328
370
|
"DateField": datetime,
|
|
371
|
+
"ApproximateDateField": datetime,
|
|
372
|
+
"IntegerField": int,
|
|
373
|
+
"DecimalField": decimal.Decimal,
|
|
329
374
|
}
|
|
330
375
|
|
|
331
376
|
|
|
@@ -334,14 +379,12 @@ def command_type_map() -> dict[str, type]:
|
|
|
334
379
|
"Command",
|
|
335
380
|
[
|
|
336
381
|
(AssessCommand),
|
|
337
|
-
|
|
338
|
-
# (DiagnoseCommand),
|
|
382
|
+
(DiagnoseCommand),
|
|
339
383
|
(GoalCommand),
|
|
340
384
|
(HistoryOfPresentIllnessCommand),
|
|
341
385
|
(MedicationStatementCommand),
|
|
342
386
|
(PlanCommand),
|
|
343
|
-
|
|
344
|
-
# (PrescribeCommand),
|
|
387
|
+
(PrescribeCommand),
|
|
345
388
|
(QuestionnaireCommand),
|
|
346
389
|
(ReasonForVisitCommand),
|
|
347
390
|
(StopMedicationCommand),
|
|
@@ -349,9 +392,9 @@ def command_type_map() -> dict[str, type]:
|
|
|
349
392
|
],
|
|
350
393
|
)
|
|
351
394
|
def test_command_schema_matches_command_api(
|
|
352
|
-
token:
|
|
395
|
+
token: MaskedValue,
|
|
353
396
|
command_type_map: dict[str, str],
|
|
354
|
-
|
|
397
|
+
note_uuid: str,
|
|
355
398
|
Command: (
|
|
356
399
|
AssessCommand
|
|
357
400
|
| DiagnoseCommand
|
|
@@ -367,8 +410,8 @@ def test_command_schema_matches_command_api(
|
|
|
367
410
|
),
|
|
368
411
|
) -> None:
|
|
369
412
|
# first create the command in the new note
|
|
370
|
-
data = {"noteKey":
|
|
371
|
-
headers = {"Authorization": f"Bearer {token}"}
|
|
413
|
+
data = {"noteKey": note_uuid, "schemaKey": Command.Meta.key}
|
|
414
|
+
headers = {"Authorization": f"Bearer {token.value}"}
|
|
372
415
|
url = f"{settings.INTEGRATION_TEST_URL}/core/api/v1/commands/"
|
|
373
416
|
command_resp = requests.post(url, headers=headers, data=data).json()
|
|
374
417
|
assert "uuid" in command_resp
|
|
@@ -396,7 +439,15 @@ def test_command_schema_matches_command_api(
|
|
|
396
439
|
expected_type = expected_field["type"]
|
|
397
440
|
if expected_type is Coding:
|
|
398
441
|
expected_type = expected_type.__annotations__["code"]
|
|
399
|
-
|
|
442
|
+
|
|
443
|
+
actual_type = command_type_map.get(actual_field["type"])
|
|
444
|
+
if actual_field["type"] == "AutocompleteField" and name[-1] == "s":
|
|
445
|
+
# this condition initially created for Prescribe.indications,
|
|
446
|
+
# but could apply to other AutocompleteField fields that are lists
|
|
447
|
+
# making the assumption here that if the field ends in 's' (like indications), it is a list
|
|
448
|
+
actual_type = list[actual_type] # type: ignore
|
|
449
|
+
|
|
450
|
+
assert expected_type == actual_type
|
|
400
451
|
|
|
401
452
|
if (choices := actual_field["choices"]) is None:
|
|
402
453
|
assert expected_field["choices"] is None
|
canvas_sdk/data/__init__.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .base import DataModel
|
canvas_sdk/data/base.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from pydantic_core import ValidationError
|
|
2
|
+
|
|
3
|
+
from canvas_sdk.base import Model
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DataModel(Model):
|
|
7
|
+
class Meta:
|
|
8
|
+
update_required_fields = ("id",)
|
|
9
|
+
|
|
10
|
+
def model_dump_json_nested(self, *args, **kwargs) -> str:
|
|
11
|
+
"""
|
|
12
|
+
Returns the model's json representation nested in a {"data": {..}} key.
|
|
13
|
+
"""
|
|
14
|
+
return f'{{"data": {self.model_dump_json(*args, **kwargs)}}}'
|
|
15
|
+
|
|
16
|
+
def _validate_before_effect(self, method: str) -> None:
|
|
17
|
+
if method == "create" and getattr(self, "id", None):
|
|
18
|
+
error = self._create_error_detail(
|
|
19
|
+
"value", "create cannot be called on a model with an id", "id"
|
|
20
|
+
)
|
|
21
|
+
raise ValidationError.from_exception_data(self.__class__.__name__, [error])
|
|
22
|
+
super()._validate_before_effect(method)
|
canvas_sdk/data/staff.py
ADDED
canvas_sdk/data/task.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
from canvas_sdk.data import DataModel
|
|
5
|
+
from canvas_sdk.data.patient import Patient
|
|
6
|
+
from canvas_sdk.data.staff import Staff
|
|
7
|
+
from canvas_sdk.effects import Effect, EffectType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Task(DataModel):
|
|
11
|
+
class Meta(DataModel.Meta):
|
|
12
|
+
create_required_fields = ("title",)
|
|
13
|
+
|
|
14
|
+
class Status(Enum):
|
|
15
|
+
COMPLETED = "Completed"
|
|
16
|
+
CLOSED = "Closed"
|
|
17
|
+
OPEN = "Open"
|
|
18
|
+
|
|
19
|
+
id: str | None = None
|
|
20
|
+
assignee: Staff | None = None
|
|
21
|
+
patient: Patient | None = None
|
|
22
|
+
title: str | None = None
|
|
23
|
+
due: datetime | None = None
|
|
24
|
+
status: Status | None = None
|
|
25
|
+
comments: "list[TaskComment] | None" = None
|
|
26
|
+
labels: list[str] | None = None
|
|
27
|
+
|
|
28
|
+
def create(self) -> Effect:
|
|
29
|
+
self._validate_before_effect("create")
|
|
30
|
+
return Effect(type=EffectType.CREATE_TASK, payload=self.model_dump_json_nested())
|
|
31
|
+
|
|
32
|
+
def update(self) -> Effect:
|
|
33
|
+
self._validate_before_effect("update")
|
|
34
|
+
payload = self.model_dump_json_nested(exclude_unset=True)
|
|
35
|
+
return Effect(type=EffectType.UPDATE_TASK, payload=payload)
|
|
36
|
+
|
|
37
|
+
def add_comment(self, comment: str) -> Effect:
|
|
38
|
+
if not self.id:
|
|
39
|
+
raise ValueError("Cannot add a comment to a Task without an id")
|
|
40
|
+
task_comment = TaskComment(task=self, body=comment)
|
|
41
|
+
task_comment._validate_before_effect("create")
|
|
42
|
+
return Effect(
|
|
43
|
+
type=EffectType.CREATE_TASK_COMMENT,
|
|
44
|
+
payload=task_comment.model_dump_json_nested(exclude_unset=True),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TaskComment(DataModel):
|
|
49
|
+
class Meta:
|
|
50
|
+
create_required_fields = (
|
|
51
|
+
"body",
|
|
52
|
+
"task",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
id: str | None = None
|
|
56
|
+
task: Task | None = None
|
|
57
|
+
body: str | None = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
Task.model_rebuild()
|
canvas_sdk/effects/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
from
|
|
1
|
+
from canvas_generated.messages.effects_pb2 import Effect, EffectType
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from pydantic import Field
|
|
5
|
+
|
|
6
|
+
from canvas_sdk.effects.base import EffectType, _BaseEffect
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AddBannerAlert(_BaseEffect):
|
|
10
|
+
"""
|
|
11
|
+
An Effect that will result in a banner alert in Canvas.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
class Meta:
|
|
15
|
+
effect_type = EffectType.ADD_BANNER_ALERT
|
|
16
|
+
|
|
17
|
+
class Placement(Enum):
|
|
18
|
+
CHART = "chart"
|
|
19
|
+
TIMELINE = "timeline"
|
|
20
|
+
APPOINTMENT_CARD = "appointment_card"
|
|
21
|
+
SCHEDULING_CARD = "scheduling_card"
|
|
22
|
+
PROFILE = "profile"
|
|
23
|
+
|
|
24
|
+
class Intent(Enum):
|
|
25
|
+
INFO = "info"
|
|
26
|
+
WARNING = "warning"
|
|
27
|
+
ALERT = "alert"
|
|
28
|
+
|
|
29
|
+
patient_id: str
|
|
30
|
+
key: str
|
|
31
|
+
narrative: str = Field(max_length=90)
|
|
32
|
+
placement: list[Placement] = Field(min_length=1)
|
|
33
|
+
intent: Intent
|
|
34
|
+
href: str | None = None
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def values(self) -> dict[str, Any]:
|
|
38
|
+
"""The BannerAlert's values."""
|
|
39
|
+
return {
|
|
40
|
+
"narrative": self.narrative,
|
|
41
|
+
"placement": [p.value for p in self.placement],
|
|
42
|
+
"intent": self.intent.value,
|
|
43
|
+
"href": self.href,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def effect_payload(self) -> dict[str, Any]:
|
|
48
|
+
"""The payload of the effect."""
|
|
49
|
+
return {"patient": self.patient_id, "key": self.key, "data": self.values}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from canvas_sdk.effects.base import EffectType, _BaseEffect
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RemoveBannerAlert(_BaseEffect):
|
|
7
|
+
"""
|
|
8
|
+
An Effect that will remove/inactivate a banner alert in Canvas.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
class Meta:
|
|
12
|
+
effect_type = EffectType.REMOVE_BANNER_ALERT
|
|
13
|
+
|
|
14
|
+
patient_id: str
|
|
15
|
+
key: str
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def effect_payload(self) -> dict[str, Any]:
|
|
19
|
+
"""The payload of the effect."""
|
|
20
|
+
return {"patient": self.patient_id, "key": self.key}
|
canvas_sdk/effects/base.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import json
|
|
1
2
|
from typing import Any
|
|
2
3
|
|
|
3
4
|
from pydantic import BaseModel, ConfigDict
|
|
4
5
|
|
|
5
|
-
from canvas_sdk.effects import Effect
|
|
6
|
+
from canvas_sdk.effects import Effect, EffectType
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class _BaseEffect(BaseModel):
|
|
@@ -11,7 +12,7 @@ class _BaseEffect(BaseModel):
|
|
|
11
12
|
"""
|
|
12
13
|
|
|
13
14
|
class Meta:
|
|
14
|
-
effect_type =
|
|
15
|
+
effect_type = EffectType.UNKNOWN_EFFECT
|
|
15
16
|
|
|
16
17
|
model_config = ConfigDict(strict=True, validate_assignment=True)
|
|
17
18
|
|
|
@@ -24,7 +25,4 @@ class _BaseEffect(BaseModel):
|
|
|
24
25
|
return {"data": self.values}
|
|
25
26
|
|
|
26
27
|
def apply(self) -> Effect:
|
|
27
|
-
return
|
|
28
|
-
"type": self.Meta.effect_type,
|
|
29
|
-
"payload": self.effect_payload,
|
|
30
|
-
}
|
|
28
|
+
return Effect(type=self.Meta.effect_type, payload=json.dumps(self.effect_payload))
|
canvas_sdk/events/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
from
|
|
1
|
+
from canvas_generated.messages.events_pb2 import Event, EventResponse, EventType
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from canvas_sdk.handlers.base import BaseHandler
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BaseHandler:
|
|
5
|
+
"""
|
|
6
|
+
The class that all handlers inherit from.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, event, secrets=None) -> None:
|
|
10
|
+
self.event = event
|
|
11
|
+
try:
|
|
12
|
+
self.context = json.loads(event.context)
|
|
13
|
+
except ValueError:
|
|
14
|
+
self.context = {}
|
|
15
|
+
self.target = event.target
|
|
16
|
+
self.secrets = secrets or {}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
|
|
3
|
+
import arrow
|
|
4
|
+
from cron_converter import Cron
|
|
5
|
+
|
|
6
|
+
from canvas_sdk.effects import Effect
|
|
7
|
+
from canvas_sdk.events import EventType
|
|
8
|
+
from canvas_sdk.handlers.base import BaseHandler
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CronTask(BaseHandler):
|
|
12
|
+
"""
|
|
13
|
+
A type of handler that executes periodically according to a provided schedule.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
RESPONDS_TO = EventType.Name(EventType.CRON)
|
|
17
|
+
|
|
18
|
+
SCHEDULE: str = "" # e.g. "* * * * *" for every minute.
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def execute(self) -> list[Effect]:
|
|
22
|
+
"""
|
|
23
|
+
Perform some work and return a list of effects.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def compute(self) -> list[Effect]:
|
|
27
|
+
"""
|
|
28
|
+
See if the task should execute given the timestamp.
|
|
29
|
+
"""
|
|
30
|
+
if not self.SCHEDULE:
|
|
31
|
+
raise ValueError("You must set a SCHEDULE.")
|
|
32
|
+
datetime = arrow.get(self.target).datetime
|
|
33
|
+
if datetime in Cron(self.SCHEDULE):
|
|
34
|
+
return self.execute()
|
|
35
|
+
return []
|
canvas_sdk/protocols/base.py
CHANGED
|
@@ -1,16 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
from canvas_sdk.handlers.base import BaseHandler
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
class BaseProtocol:
|
|
4
|
+
class BaseProtocol(BaseHandler):
|
|
5
5
|
"""
|
|
6
6
|
The class that protocols inherit from.
|
|
7
7
|
"""
|
|
8
|
-
|
|
9
|
-
def __init__(self, event, secrets=None) -> None:
|
|
10
|
-
self.event = event
|
|
11
|
-
try:
|
|
12
|
-
self.context = json.loads(event.context)
|
|
13
|
-
except ValueError:
|
|
14
|
-
self.context = {}
|
|
15
|
-
self.target = event.target
|
|
16
|
-
self.secrets = secrets or {}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from time import time
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_duration_ms(start_time: time) -> int:
|
|
6
|
+
return int((time() - start_time) * 1000)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
LINE_PROTOCOL_TRANSLATION = str.maketrans(
|
|
10
|
+
{
|
|
11
|
+
",": r"\,",
|
|
12
|
+
"=": r"\=",
|
|
13
|
+
" ": r"\ ",
|
|
14
|
+
":": r"__",
|
|
15
|
+
}
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def tags_to_line_protocol(tags: dict[str, Any]) -> str:
|
|
20
|
+
"""Generate a tags string compatible with the InfluxDB line protocol.
|
|
21
|
+
|
|
22
|
+
See: https://docs.influxdata.com/influxdb/v1.1/write_protocols/line_protocol_tutorial/
|
|
23
|
+
"""
|
|
24
|
+
return ",".join(
|
|
25
|
+
f"{tag_name}={str(tag_value).translate(LINE_PROTOCOL_TRANSLATION)}"
|
|
26
|
+
for tag_name, tag_value in tags.items()
|
|
27
|
+
)
|
logger/__init__.py
ADDED