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.

Files changed (29) hide show
  1. {canvas-0.69.0.dist-info → canvas-0.71.0.dist-info}/METADATA +6 -6
  2. {canvas-0.69.0.dist-info → canvas-0.71.0.dist-info}/RECORD +29 -28
  3. canvas_cli/apps/emit/event_fixtures/UNKNOWN.ndjson +1 -0
  4. canvas_generated/messages/effects_pb2.py +15 -5
  5. canvas_generated/messages/effects_pb2.pyi +2 -0
  6. canvas_generated/messages/effects_pb2_grpc.py +20 -0
  7. canvas_generated/messages/events_pb2.py +15 -5
  8. canvas_generated/messages/events_pb2.pyi +8 -1
  9. canvas_generated/messages/events_pb2_grpc.py +20 -0
  10. canvas_generated/messages/plugins_pb2.py +13 -3
  11. canvas_generated/messages/plugins_pb2_grpc.py +20 -0
  12. canvas_generated/services/plugin_runner_pb2.py +13 -3
  13. canvas_generated/services/plugin_runner_pb2_grpc.py +77 -16
  14. canvas_sdk/commands/commands/family_history.py +17 -1
  15. canvas_sdk/commands/commands/immunization_statement.py +44 -2
  16. canvas_sdk/commands/commands/medication_statement.py +16 -1
  17. canvas_sdk/commands/commands/past_surgical_history.py +16 -1
  18. canvas_sdk/commands/commands/perform.py +18 -1
  19. canvas_sdk/effects/note/message.py +22 -14
  20. canvas_sdk/templates/utils.py +17 -4
  21. canvas_sdk/v1/data/detected_issue.py +18 -2
  22. canvas_sdk/v1/data/medication_statement.py +1 -1
  23. canvas_sdk/v1/data/message.py +1 -1
  24. plugin_runner/plugin_runner.py +1 -0
  25. protobufs/canvas_generated/messages/effects.proto +1 -0
  26. protobufs/canvas_generated/messages/events.proto +4 -0
  27. settings.py +0 -9
  28. {canvas-0.69.0.dist-info → canvas-0.71.0.dist-info}/WHEEL +0 -0
  29. {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(default=None, json_schema_extra={"commands_api_name": "perform"})
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
- method
73
- in (
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
- errors.append(
80
- self._create_error_detail(
81
- "value",
82
- "Can't set message ID when creating a message.",
83
- self.message_id,
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
 
@@ -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 Context, Template
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
- template = Template(template_path.read_text())
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=255, default="")
38
+ sig_original_input = models.CharField(max_length=1000, default="")
39
39
 
40
40
 
41
41
  __exports__ = ("MedicationStatement",)
@@ -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.BooleanField()
31
+ read = models.DateTimeField(null=True, blank=True)
32
32
 
33
33
 
34
34
  class MessageAttachment(IdentifiableModel):
@@ -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())
@@ -181,6 +181,7 @@ enum EffectType {
181
181
  CREATE_QUESTIONNAIRE_RESULT = 138;
182
182
 
183
183
  ANNOTATE_PATIENT_CHART_CONDITION_RESULTS = 200;
184
+ ANNOTATE_PATIENT_CHART_DETECTED_ISSUE_RESULTS = 201;
184
185
 
185
186
  ANNOTATE_CLAIM_CONDITION_RESULTS = 300;
186
187
 
@@ -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 = {