canvas 0.69.0__py3-none-any.whl → 0.71.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.
- {canvas-0.69.0.dist-info → canvas-0.71.0.dist-info}/METADATA +6 -6
- {canvas-0.69.0.dist-info → canvas-0.71.0.dist-info}/RECORD +29 -28
- canvas_cli/apps/emit/event_fixtures/UNKNOWN.ndjson +1 -0
- canvas_generated/messages/effects_pb2.py +15 -5
- canvas_generated/messages/effects_pb2.pyi +2 -0
- canvas_generated/messages/effects_pb2_grpc.py +20 -0
- canvas_generated/messages/events_pb2.py +15 -5
- canvas_generated/messages/events_pb2.pyi +8 -1
- canvas_generated/messages/events_pb2_grpc.py +20 -0
- canvas_generated/messages/plugins_pb2.py +13 -3
- canvas_generated/messages/plugins_pb2_grpc.py +20 -0
- canvas_generated/services/plugin_runner_pb2.py +13 -3
- canvas_generated/services/plugin_runner_pb2_grpc.py +77 -16
- canvas_sdk/commands/commands/family_history.py +17 -1
- canvas_sdk/commands/commands/immunization_statement.py +44 -2
- canvas_sdk/commands/commands/medication_statement.py +16 -1
- canvas_sdk/commands/commands/past_surgical_history.py +16 -1
- canvas_sdk/commands/commands/perform.py +18 -1
- canvas_sdk/effects/note/message.py +22 -14
- canvas_sdk/templates/utils.py +17 -4
- canvas_sdk/v1/data/detected_issue.py +18 -2
- canvas_sdk/v1/data/medication_statement.py +1 -1
- canvas_sdk/v1/data/message.py +1 -1
- plugin_runner/plugin_runner.py +1 -0
- protobufs/canvas_generated/messages/effects.proto +1 -0
- protobufs/canvas_generated/messages/events.proto +4 -0
- settings.py +0 -9
- {canvas-0.69.0.dist-info → canvas-0.71.0.dist-info}/WHEEL +0 -0
- {canvas-0.69.0.dist-info → canvas-0.71.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
from pydantic_core import InitErrorDetails
|
|
2
|
+
|
|
1
3
|
from canvas_sdk.commands.base import _BaseCommand as BaseCommand
|
|
4
|
+
from canvas_sdk.commands.constants import CodeSystems, Coding
|
|
2
5
|
|
|
3
6
|
|
|
4
7
|
class FamilyHistoryCommand(BaseCommand):
|
|
@@ -7,9 +10,22 @@ class FamilyHistoryCommand(BaseCommand):
|
|
|
7
10
|
class Meta:
|
|
8
11
|
key = "familyHistory"
|
|
9
12
|
|
|
10
|
-
family_history: str | None = None
|
|
13
|
+
family_history: str | Coding | None = None
|
|
11
14
|
relative: str | None = None
|
|
12
15
|
note: str | None = None
|
|
13
16
|
|
|
17
|
+
def _get_error_details(self, method: str) -> list[InitErrorDetails]:
|
|
18
|
+
errors = super()._get_error_details(method)
|
|
19
|
+
|
|
20
|
+
if (
|
|
21
|
+
isinstance(self.family_history, dict)
|
|
22
|
+
and self.family_history["system"] != CodeSystems.SNOMED
|
|
23
|
+
and self.family_history["system"] != CodeSystems.UNSTRUCTURED
|
|
24
|
+
):
|
|
25
|
+
message = f"The 'coding.system' field must be '{CodeSystems.SNOMED}' or '{CodeSystems.UNSTRUCTURED}'."
|
|
26
|
+
errors.append(self._create_error_detail("value", message, self.family_history))
|
|
27
|
+
|
|
28
|
+
return errors
|
|
29
|
+
|
|
14
30
|
|
|
15
31
|
__exports__ = ("FamilyHistoryCommand",)
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
from datetime import date
|
|
2
|
+
from typing import Self
|
|
2
3
|
|
|
4
|
+
from pydantic import model_validator
|
|
3
5
|
from pydantic_core import InitErrorDetails
|
|
4
6
|
|
|
5
7
|
from canvas_sdk.commands.base import _BaseCommand as BaseCommand
|
|
8
|
+
from canvas_sdk.commands.constants import CodeSystems, Coding
|
|
6
9
|
|
|
7
10
|
|
|
8
11
|
class ImmunizationStatementCommand(BaseCommand):
|
|
@@ -11,14 +14,53 @@ class ImmunizationStatementCommand(BaseCommand):
|
|
|
11
14
|
class Meta:
|
|
12
15
|
key = "immunizationStatement"
|
|
13
16
|
|
|
14
|
-
cpt_code: str
|
|
15
|
-
cvx_code: str
|
|
17
|
+
cpt_code: str | Coding | None = None
|
|
18
|
+
cvx_code: str | Coding | None = None
|
|
19
|
+
unstructured: Coding | None = None
|
|
16
20
|
approximate_date: date | None = None
|
|
17
21
|
comments: str | None = None
|
|
18
22
|
|
|
23
|
+
def _has_value(self, value: str | Coding | None) -> bool:
|
|
24
|
+
"""Check if a value is set."""
|
|
25
|
+
if value is None:
|
|
26
|
+
return False
|
|
27
|
+
if isinstance(value, str):
|
|
28
|
+
return value.strip() != ""
|
|
29
|
+
# For Coding objects (dicts), check if it exists
|
|
30
|
+
return True
|
|
31
|
+
|
|
32
|
+
@model_validator(mode="after")
|
|
33
|
+
def check_needed_together_fields(self) -> Self:
|
|
34
|
+
"""Check that both 'cpt_code' and 'cvx_code' are set if one is provided."""
|
|
35
|
+
has_cpt_code = self._has_value(self.cpt_code)
|
|
36
|
+
has_cvx_code = self._has_value(self.cvx_code)
|
|
37
|
+
|
|
38
|
+
if has_cpt_code ^ has_cvx_code:
|
|
39
|
+
raise ValueError(
|
|
40
|
+
"Both cpt_code and cvx_code must be provided if one is specified and cannot be empty."
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return self
|
|
44
|
+
|
|
19
45
|
def _get_error_details(self, method: str) -> list[InitErrorDetails]:
|
|
20
46
|
errors = super()._get_error_details(method)
|
|
21
47
|
|
|
48
|
+
if self.unstructured and (self.cpt_code or self.cvx_code):
|
|
49
|
+
message = "Unstructured codes cannot be used with CPT or CVX codes."
|
|
50
|
+
errors.append(self._create_error_detail("value", message, self.cpt_code))
|
|
51
|
+
|
|
52
|
+
if isinstance(self.cpt_code, dict) and self.cpt_code["system"] != CodeSystems.CPT:
|
|
53
|
+
message = f"The 'cpt_code.system' field must be '{CodeSystems.CPT}'"
|
|
54
|
+
errors.append(self._create_error_detail("value", message, self.cpt_code))
|
|
55
|
+
|
|
56
|
+
if isinstance(self.cvx_code, dict) and self.cvx_code["system"] != CodeSystems.CVX:
|
|
57
|
+
message = f"The 'cvx_code.system' field must be '{CodeSystems.CVX}'"
|
|
58
|
+
errors.append(self._create_error_detail("value", message, self.cvx_code))
|
|
59
|
+
|
|
60
|
+
if self.unstructured and self.unstructured["system"] != CodeSystems.UNSTRUCTURED:
|
|
61
|
+
message = f"The 'unstructured.system' field must be '{CodeSystems.UNSTRUCTURED}'"
|
|
62
|
+
errors.append(self._create_error_detail("value", message, self.unstructured))
|
|
63
|
+
|
|
22
64
|
if self.comments and len(self.comments) > 255:
|
|
23
65
|
errors.append(
|
|
24
66
|
self._create_error_detail(
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from pydantic import Field
|
|
2
|
+
from pydantic_core import InitErrorDetails
|
|
2
3
|
|
|
3
4
|
from canvas_sdk.commands.base import _BaseCommand
|
|
5
|
+
from canvas_sdk.commands.constants import CodeSystems, Coding
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
class MedicationStatementCommand(_BaseCommand):
|
|
@@ -9,11 +11,24 @@ class MedicationStatementCommand(_BaseCommand):
|
|
|
9
11
|
class Meta:
|
|
10
12
|
key = "medicationStatement"
|
|
11
13
|
|
|
12
|
-
fdb_code: str | None = Field(
|
|
14
|
+
fdb_code: str | Coding | None = Field(
|
|
13
15
|
default=None, json_schema_extra={"commands_api_name": "medication"}
|
|
14
16
|
)
|
|
15
17
|
sig: str | None = None
|
|
16
18
|
|
|
19
|
+
def _get_error_details(self, method: str) -> list[InitErrorDetails]:
|
|
20
|
+
errors = super()._get_error_details(method)
|
|
21
|
+
|
|
22
|
+
if (
|
|
23
|
+
isinstance(self.fdb_code, dict)
|
|
24
|
+
and self.fdb_code["system"] != CodeSystems.FDB
|
|
25
|
+
and self.fdb_code["system"] != CodeSystems.UNSTRUCTURED
|
|
26
|
+
):
|
|
27
|
+
message = f"The 'coding.system' field must be '{CodeSystems.FDB}' or '{CodeSystems.UNSTRUCTURED}'."
|
|
28
|
+
errors.append(self._create_error_detail("value", message, self.fdb_code))
|
|
29
|
+
|
|
30
|
+
return errors
|
|
31
|
+
|
|
17
32
|
|
|
18
33
|
# how do we make sure fdb_code is a valid code?
|
|
19
34
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from datetime import date
|
|
2
2
|
|
|
3
3
|
from pydantic import Field
|
|
4
|
+
from pydantic_core import InitErrorDetails
|
|
4
5
|
|
|
5
6
|
from canvas_sdk.commands.base import _BaseCommand as BaseCommand
|
|
7
|
+
from canvas_sdk.commands.constants import CodeSystems, Coding
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
class PastSurgicalHistoryCommand(BaseCommand):
|
|
@@ -11,9 +13,22 @@ class PastSurgicalHistoryCommand(BaseCommand):
|
|
|
11
13
|
class Meta:
|
|
12
14
|
key = "surgicalHistory"
|
|
13
15
|
|
|
14
|
-
past_surgical_history: str | None = None
|
|
16
|
+
past_surgical_history: str | Coding | None = None
|
|
15
17
|
approximate_date: date | None = None
|
|
16
18
|
comment: str | None = Field(max_length=1000, default=None)
|
|
17
19
|
|
|
20
|
+
def _get_error_details(self, method: str) -> list[InitErrorDetails]:
|
|
21
|
+
errors = super()._get_error_details(method)
|
|
22
|
+
|
|
23
|
+
if (
|
|
24
|
+
isinstance(self.past_surgical_history, dict)
|
|
25
|
+
and self.past_surgical_history["system"] != CodeSystems.SNOMED
|
|
26
|
+
and self.past_surgical_history["system"] != CodeSystems.UNSTRUCTURED
|
|
27
|
+
):
|
|
28
|
+
message = f"The 'coding.system' field must be '{CodeSystems.SNOMED}' or '{CodeSystems.UNSTRUCTURED}'."
|
|
29
|
+
errors.append(self._create_error_detail("value", message, self.past_surgical_history))
|
|
30
|
+
|
|
31
|
+
return errors
|
|
32
|
+
|
|
18
33
|
|
|
19
34
|
__exports__ = ("PastSurgicalHistoryCommand",)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from pydantic import Field
|
|
2
|
+
from pydantic_core import InitErrorDetails
|
|
2
3
|
|
|
3
4
|
from canvas_sdk.commands.base import _BaseCommand as BaseCommand
|
|
5
|
+
from canvas_sdk.commands.constants import CodeSystems, Coding
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
class PerformCommand(BaseCommand):
|
|
@@ -9,8 +11,23 @@ class PerformCommand(BaseCommand):
|
|
|
9
11
|
class Meta:
|
|
10
12
|
key = "perform"
|
|
11
13
|
|
|
12
|
-
cpt_code: str | None = Field(
|
|
14
|
+
cpt_code: str | Coding | None = Field(
|
|
15
|
+
default=None, json_schema_extra={"commands_api_name": "perform"}
|
|
16
|
+
)
|
|
13
17
|
notes: str | None = None
|
|
14
18
|
|
|
19
|
+
def _get_error_details(self, method: str) -> list[InitErrorDetails]:
|
|
20
|
+
errors = super()._get_error_details(method)
|
|
21
|
+
|
|
22
|
+
if (
|
|
23
|
+
isinstance(self.cpt_code, dict)
|
|
24
|
+
and self.cpt_code["system"] != CodeSystems.CPT
|
|
25
|
+
and self.cpt_code["system"] != CodeSystems.UNSTRUCTURED
|
|
26
|
+
):
|
|
27
|
+
message = f"The 'coding.system' field must be '{CodeSystems.CPT}' or '{CodeSystems.UNSTRUCTURED}'."
|
|
28
|
+
errors.append(self._create_error_detail("value", message, self.cpt_code))
|
|
29
|
+
|
|
30
|
+
return errors
|
|
31
|
+
|
|
15
32
|
|
|
16
33
|
__exports__ = ("PerformCommand",)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
from datetime import datetime
|
|
2
3
|
from typing import Any
|
|
3
4
|
from uuid import UUID
|
|
4
5
|
|
|
@@ -19,9 +20,10 @@ class Message(TrackableFieldsModel):
|
|
|
19
20
|
effect_type = "MESSAGE"
|
|
20
21
|
|
|
21
22
|
message_id: str | UUID | None = None
|
|
22
|
-
content: str
|
|
23
|
+
content: str | None
|
|
23
24
|
sender_id: str | UUID
|
|
24
25
|
recipient_id: str | UUID
|
|
26
|
+
read: datetime | None = None
|
|
25
27
|
|
|
26
28
|
def _get_error_details(self, method: Any) -> list[InitErrorDetails]:
|
|
27
29
|
errors = super()._get_error_details(method)
|
|
@@ -68,21 +70,27 @@ class Message(TrackableFieldsModel):
|
|
|
68
70
|
self.message_id,
|
|
69
71
|
)
|
|
70
72
|
)
|
|
71
|
-
elif (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
"create",
|
|
75
|
-
"create_and_send",
|
|
76
|
-
)
|
|
77
|
-
and self.message_id
|
|
73
|
+
elif method in (
|
|
74
|
+
"create",
|
|
75
|
+
"create_and_send",
|
|
78
76
|
):
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
77
|
+
if not self.content or self.content.strip() == "":
|
|
78
|
+
errors.append(
|
|
79
|
+
self._create_error_detail(
|
|
80
|
+
"value",
|
|
81
|
+
"Message content cannot be empty.",
|
|
82
|
+
self.content,
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if self.message_id:
|
|
87
|
+
errors.append(
|
|
88
|
+
self._create_error_detail(
|
|
89
|
+
"value",
|
|
90
|
+
"Can't set message ID when creating a message.",
|
|
91
|
+
self.message_id,
|
|
92
|
+
)
|
|
84
93
|
)
|
|
85
|
-
)
|
|
86
94
|
|
|
87
95
|
return errors
|
|
88
96
|
|
canvas_sdk/templates/utils.py
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
|
+
from functools import cache, lru_cache
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
from typing import Any
|
|
3
4
|
|
|
4
|
-
from django.template import
|
|
5
|
+
from django.template.backends.django import get_installed_libraries
|
|
6
|
+
from django.template.engine import Engine
|
|
5
7
|
|
|
6
8
|
from canvas_sdk.utils.plugins import plugin_context
|
|
7
9
|
|
|
8
10
|
|
|
11
|
+
@cache
|
|
12
|
+
def _installed_template_libraries() -> dict[str, str]:
|
|
13
|
+
"""Cache Django's template tag libraries lookup."""
|
|
14
|
+
return get_installed_libraries()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@lru_cache(maxsize=5)
|
|
18
|
+
def _engine_for_plugin(plugin_dir: str) -> Engine:
|
|
19
|
+
"""Create a Django template engine for the given plugin directory."""
|
|
20
|
+
return Engine(dirs=[plugin_dir], libraries=_installed_template_libraries())
|
|
21
|
+
|
|
22
|
+
|
|
9
23
|
@plugin_context
|
|
10
24
|
def render_to_string(
|
|
11
25
|
template_name: str,
|
|
@@ -36,9 +50,8 @@ def render_to_string(
|
|
|
36
50
|
elif not template_path.exists():
|
|
37
51
|
raise FileNotFoundError(f"Template {template_name} not found.")
|
|
38
52
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return template.render(Context(context))
|
|
53
|
+
engine = _engine_for_plugin(plugin_dir)
|
|
54
|
+
return engine.render_to_string(str(template_path), context=context)
|
|
42
55
|
|
|
43
56
|
|
|
44
57
|
__exports__ = ("render_to_string",)
|
|
@@ -10,13 +10,29 @@ class DetectedIssue(AuditedModel, IdentifiableModel):
|
|
|
10
10
|
class Meta:
|
|
11
11
|
db_table = "canvas_sdk_data_api_detectedissue_001"
|
|
12
12
|
|
|
13
|
+
class Status(models.TextChoices):
|
|
14
|
+
REGISTERED = "registered", "Registered"
|
|
15
|
+
PRELIMINARY = "preliminary", "Preliminary"
|
|
16
|
+
CANCELLED = "cancelled", "Cancelled"
|
|
17
|
+
AMENDED = "amended", "Amended"
|
|
18
|
+
FINAL = "final", "Final"
|
|
19
|
+
CORRECTED = "corrected", "Corrected"
|
|
20
|
+
ENTERED_IN_ERROR = "entered-in-error", "Entered in Error"
|
|
21
|
+
|
|
22
|
+
class Severity(models.TextChoices):
|
|
23
|
+
HIGH = "high", "High"
|
|
24
|
+
MODERATE = "moderate", "Moderate"
|
|
25
|
+
LOW = "low", "Low"
|
|
26
|
+
|
|
27
|
+
created = models.DateTimeField(auto_now_add=True)
|
|
28
|
+
modified = models.DateTimeField(auto_now=True)
|
|
13
29
|
identified = models.DateTimeField()
|
|
14
30
|
patient = models.ForeignKey(
|
|
15
31
|
"v1.Patient", on_delete=models.DO_NOTHING, related_name="detected_issues", null=True
|
|
16
32
|
)
|
|
17
33
|
code = models.CharField(max_length=20)
|
|
18
|
-
status = models.CharField(max_length=16)
|
|
19
|
-
severity = models.CharField(max_length=10)
|
|
34
|
+
status = models.CharField(max_length=16, choices=Status.choices)
|
|
35
|
+
severity = models.CharField(max_length=10, choices=Severity.choices, blank=True, default="")
|
|
20
36
|
reference = models.CharField(max_length=200)
|
|
21
37
|
issue_identifier = models.CharField(max_length=255)
|
|
22
38
|
issue_identifier_system = models.CharField(max_length=255)
|
|
@@ -35,7 +35,7 @@ class MedicationStatement(AuditedModel, IdentifiableModel):
|
|
|
35
35
|
dose_route = models.CharField(max_length=255, default="")
|
|
36
36
|
dose_frequency = models.FloatField(null=True)
|
|
37
37
|
dose_frequency_interval = models.CharField(max_length=255, default="")
|
|
38
|
-
sig_original_input = models.CharField(max_length=
|
|
38
|
+
sig_original_input = models.CharField(max_length=1000, default="")
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
__exports__ = ("MedicationStatement",)
|
canvas_sdk/v1/data/message.py
CHANGED
|
@@ -28,7 +28,7 @@ class Message(TimestampedModel, IdentifiableModel):
|
|
|
28
28
|
note = models.ForeignKey(
|
|
29
29
|
"v1.Note", on_delete=models.DO_NOTHING, related_name="message", null=True
|
|
30
30
|
)
|
|
31
|
-
read = models.
|
|
31
|
+
read = models.DateTimeField(null=True, blank=True)
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
class MessageAttachment(IdentifiableModel):
|
plugin_runner/plugin_runner.py
CHANGED
|
@@ -429,6 +429,7 @@ def synchronize_plugins(run_once: bool = False) -> None:
|
|
|
429
429
|
log.info(
|
|
430
430
|
f'synchronize_plugins: installing/reloading plugin "{plugin_name}" for action=reload'
|
|
431
431
|
)
|
|
432
|
+
unload_plugin(plugin_name)
|
|
432
433
|
install_plugin(plugin_name, attributes=plugin)
|
|
433
434
|
plugin_dir = pathlib.Path(PLUGIN_DIRECTORY) / plugin_name
|
|
434
435
|
load_plugin(plugin_dir.resolve())
|
|
@@ -48,6 +48,8 @@ enum EventType {
|
|
|
48
48
|
MEDICATION_LIST_ITEM_CREATED = 40;
|
|
49
49
|
MEDICATION_LIST_ITEM_UPDATED = 41;
|
|
50
50
|
MESSAGE_CREATED = 42;
|
|
51
|
+
MESSAGE_TRANSMISSION_CREATED = 53;
|
|
52
|
+
MESSAGE_TRANSMISSION_UPDATED = 56;
|
|
51
53
|
PATIENT_CREATED = 43;
|
|
52
54
|
PATIENT_UPDATED = 44;
|
|
53
55
|
PRESCRIPTION_CREATED = 45;
|
|
@@ -1110,6 +1112,8 @@ enum EventType {
|
|
|
1110
1112
|
ACTION_BUTTON_CLICKED = 70002;
|
|
1111
1113
|
|
|
1112
1114
|
PATIENT_CHART__CONDITIONS = 100000;
|
|
1115
|
+
PATIENT_CHART__DETECTED_ISSUES = 100005;
|
|
1116
|
+
|
|
1113
1117
|
PATIENT_CHART_SUMMARY__SECTION_CONFIGURATION = 100001;
|
|
1114
1118
|
|
|
1115
1119
|
PATIENT_PROFILE__SECTION_CONFIGURATION = 100002;
|
settings.py
CHANGED
|
@@ -172,15 +172,6 @@ SECRETS_FILE_NAME = "SECRETS.json"
|
|
|
172
172
|
|
|
173
173
|
SENTRY_DSN = os.getenv("SENTRY_DSN")
|
|
174
174
|
|
|
175
|
-
TEMPLATES = [
|
|
176
|
-
{
|
|
177
|
-
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
|
178
|
-
"DIRS": [],
|
|
179
|
-
"APP_DIRS": False,
|
|
180
|
-
"OPTIONS": {},
|
|
181
|
-
},
|
|
182
|
-
]
|
|
183
|
-
|
|
184
175
|
|
|
185
176
|
if IS_SCRIPT:
|
|
186
177
|
CACHES = {
|
|
File without changes
|
|
File without changes
|