canvas 0.63.0__py3-none-any.whl → 0.89.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.
- {canvas-0.63.0.dist-info → canvas-0.89.0.dist-info}/METADATA +4 -1
- {canvas-0.63.0.dist-info → canvas-0.89.0.dist-info}/RECORD +184 -98
- {canvas-0.63.0.dist-info → canvas-0.89.0.dist-info}/WHEEL +1 -1
- canvas_cli/apps/emit/event_fixtures/UNKNOWN.ndjson +1 -0
- canvas_cli/apps/logs/logs.py +386 -22
- canvas_cli/main.py +3 -1
- canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/tests/test_models.py +46 -4
- canvas_cli/utils/context/context.py +13 -13
- canvas_cli/utils/validators/manifest_schema.py +26 -1
- canvas_generated/messages/effects_pb2.py +5 -5
- canvas_generated/messages/effects_pb2.pyi +108 -2
- canvas_generated/messages/events_pb2.py +6 -6
- canvas_generated/messages/events_pb2.pyi +282 -2
- canvas_sdk/clients/__init__.py +1 -0
- canvas_sdk/clients/llms/__init__.py +17 -0
- canvas_sdk/clients/llms/libraries/__init__.py +11 -0
- canvas_sdk/clients/llms/libraries/llm_anthropic.py +87 -0
- canvas_sdk/clients/llms/libraries/llm_api.py +143 -0
- canvas_sdk/clients/llms/libraries/llm_google.py +92 -0
- canvas_sdk/clients/llms/libraries/llm_openai.py +98 -0
- canvas_sdk/clients/llms/structures/__init__.py +9 -0
- canvas_sdk/clients/llms/structures/llm_response.py +33 -0
- canvas_sdk/clients/llms/structures/llm_tokens.py +53 -0
- canvas_sdk/clients/llms/structures/llm_turn.py +47 -0
- canvas_sdk/clients/llms/structures/settings/__init__.py +13 -0
- canvas_sdk/clients/llms/structures/settings/llm_settings.py +27 -0
- canvas_sdk/clients/llms/structures/settings/llm_settings_anthropic.py +43 -0
- canvas_sdk/clients/llms/structures/settings/llm_settings_gemini.py +40 -0
- canvas_sdk/clients/llms/structures/settings/llm_settings_gpt4.py +40 -0
- canvas_sdk/clients/llms/structures/settings/llm_settings_gpt5.py +48 -0
- canvas_sdk/clients/third_party.py +3 -0
- canvas_sdk/commands/__init__.py +12 -0
- canvas_sdk/commands/base.py +33 -2
- canvas_sdk/commands/commands/adjust_prescription.py +4 -0
- canvas_sdk/commands/commands/custom_command.py +86 -0
- canvas_sdk/commands/commands/family_history.py +17 -1
- canvas_sdk/commands/commands/immunization_statement.py +42 -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/commands/commands/prescribe.py +8 -9
- canvas_sdk/commands/commands/refill.py +5 -5
- canvas_sdk/commands/commands/resolve_condition.py +5 -5
- canvas_sdk/commands/commands/review/__init__.py +3 -0
- canvas_sdk/commands/commands/review/base.py +72 -0
- canvas_sdk/commands/commands/review/imaging.py +13 -0
- canvas_sdk/commands/commands/review/lab.py +13 -0
- canvas_sdk/commands/commands/review/referral.py +13 -0
- canvas_sdk/commands/commands/review/uncategorized_document.py +13 -0
- canvas_sdk/commands/validation.py +43 -0
- canvas_sdk/effects/batch_originate.py +22 -0
- canvas_sdk/effects/calendar/__init__.py +13 -3
- canvas_sdk/effects/calendar/{create_calendar.py → calendar.py} +19 -5
- canvas_sdk/effects/calendar/event.py +172 -0
- canvas_sdk/effects/claim_label.py +93 -0
- canvas_sdk/effects/claim_line_item.py +47 -0
- canvas_sdk/effects/claim_queue.py +49 -0
- canvas_sdk/effects/fax/__init__.py +3 -0
- canvas_sdk/effects/fax/base.py +77 -0
- canvas_sdk/effects/fax/note.py +42 -0
- canvas_sdk/effects/metadata.py +15 -1
- canvas_sdk/effects/note/__init__.py +8 -1
- canvas_sdk/effects/note/appointment.py +135 -7
- canvas_sdk/effects/note/base.py +17 -0
- canvas_sdk/effects/note/message.py +22 -14
- canvas_sdk/effects/note/note.py +150 -1
- canvas_sdk/effects/observation/__init__.py +11 -0
- canvas_sdk/effects/observation/base.py +206 -0
- canvas_sdk/effects/patient/__init__.py +2 -0
- canvas_sdk/effects/patient/base.py +8 -0
- canvas_sdk/effects/payment/__init__.py +11 -0
- canvas_sdk/effects/payment/base.py +355 -0
- canvas_sdk/effects/payment/post_claim_payment.py +49 -0
- canvas_sdk/effects/send_contact_verification.py +42 -0
- canvas_sdk/effects/task/__init__.py +2 -1
- canvas_sdk/effects/task/task.py +30 -0
- canvas_sdk/effects/validation/__init__.py +3 -0
- canvas_sdk/effects/validation/base.py +92 -0
- canvas_sdk/events/base.py +15 -0
- canvas_sdk/handlers/application.py +7 -7
- canvas_sdk/handlers/simple_api/api.py +1 -4
- canvas_sdk/handlers/simple_api/websocket.py +1 -4
- canvas_sdk/handlers/utils.py +14 -0
- canvas_sdk/questionnaires/utils.py +1 -0
- canvas_sdk/templates/utils.py +17 -4
- canvas_sdk/test_utils/factories/FACTORY_GUIDE.md +362 -0
- canvas_sdk/test_utils/factories/__init__.py +115 -0
- canvas_sdk/test_utils/factories/calendar.py +24 -0
- canvas_sdk/test_utils/factories/claim.py +81 -0
- canvas_sdk/test_utils/factories/claim_diagnosis_code.py +16 -0
- canvas_sdk/test_utils/factories/coverage.py +17 -0
- canvas_sdk/test_utils/factories/imaging.py +74 -0
- canvas_sdk/test_utils/factories/lab.py +192 -0
- canvas_sdk/test_utils/factories/medication_history.py +75 -0
- canvas_sdk/test_utils/factories/note.py +52 -0
- canvas_sdk/test_utils/factories/organization.py +50 -0
- canvas_sdk/test_utils/factories/practicelocation.py +88 -0
- canvas_sdk/test_utils/factories/referral.py +81 -0
- canvas_sdk/test_utils/factories/staff.py +111 -0
- canvas_sdk/test_utils/factories/task.py +66 -0
- canvas_sdk/test_utils/factories/uncategorized_clinical_document.py +48 -0
- canvas_sdk/utils/metrics.py +4 -1
- canvas_sdk/v1/data/__init__.py +66 -7
- canvas_sdk/v1/data/allergy_intolerance.py +5 -11
- canvas_sdk/v1/data/appointment.py +18 -4
- canvas_sdk/v1/data/assessment.py +2 -12
- canvas_sdk/v1/data/banner_alert.py +2 -4
- canvas_sdk/v1/data/base.py +53 -14
- canvas_sdk/v1/data/billing.py +8 -11
- canvas_sdk/v1/data/calendar.py +64 -0
- canvas_sdk/v1/data/care_team.py +4 -10
- canvas_sdk/v1/data/claim.py +172 -66
- canvas_sdk/v1/data/claim_diagnosis_code.py +19 -0
- canvas_sdk/v1/data/claim_line_item.py +2 -5
- canvas_sdk/v1/data/coding.py +19 -0
- canvas_sdk/v1/data/command.py +2 -4
- canvas_sdk/v1/data/common.py +10 -0
- canvas_sdk/v1/data/compound_medication.py +3 -4
- canvas_sdk/v1/data/condition.py +4 -9
- canvas_sdk/v1/data/coverage.py +66 -26
- canvas_sdk/v1/data/detected_issue.py +20 -20
- canvas_sdk/v1/data/device.py +2 -14
- canvas_sdk/v1/data/discount.py +2 -5
- canvas_sdk/v1/data/encounter.py +44 -0
- canvas_sdk/v1/data/facility.py +1 -0
- canvas_sdk/v1/data/goal.py +2 -14
- canvas_sdk/v1/data/imaging.py +4 -30
- canvas_sdk/v1/data/immunization.py +7 -15
- canvas_sdk/v1/data/lab.py +12 -65
- canvas_sdk/v1/data/line_item_transaction.py +2 -5
- canvas_sdk/v1/data/medication.py +3 -8
- canvas_sdk/v1/data/medication_history.py +142 -0
- canvas_sdk/v1/data/medication_statement.py +41 -0
- canvas_sdk/v1/data/message.py +4 -8
- canvas_sdk/v1/data/note.py +37 -38
- canvas_sdk/v1/data/observation.py +9 -36
- canvas_sdk/v1/data/organization.py +70 -9
- canvas_sdk/v1/data/patient.py +8 -12
- canvas_sdk/v1/data/patient_consent.py +4 -14
- canvas_sdk/v1/data/payment_collection.py +2 -5
- canvas_sdk/v1/data/posting.py +3 -9
- canvas_sdk/v1/data/practicelocation.py +66 -7
- canvas_sdk/v1/data/protocol_override.py +3 -4
- canvas_sdk/v1/data/protocol_result.py +3 -3
- canvas_sdk/v1/data/questionnaire.py +10 -26
- canvas_sdk/v1/data/reason_for_visit.py +2 -6
- canvas_sdk/v1/data/referral.py +41 -17
- canvas_sdk/v1/data/staff.py +34 -26
- canvas_sdk/v1/data/stop_medication_event.py +27 -0
- canvas_sdk/v1/data/task.py +30 -11
- canvas_sdk/v1/data/team.py +2 -4
- canvas_sdk/v1/data/uncategorized_clinical_document.py +84 -0
- canvas_sdk/v1/data/user.py +14 -0
- canvas_sdk/v1/data/utils.py +5 -0
- canvas_sdk/value_set/v2026/__init__.py +1 -0
- canvas_sdk/value_set/v2026/adverse_event.py +157 -0
- canvas_sdk/value_set/v2026/allergy.py +116 -0
- canvas_sdk/value_set/v2026/assessment.py +466 -0
- canvas_sdk/value_set/v2026/communication.py +496 -0
- canvas_sdk/value_set/v2026/condition.py +52934 -0
- canvas_sdk/value_set/v2026/device.py +315 -0
- canvas_sdk/value_set/v2026/diagnostic_study.py +5243 -0
- canvas_sdk/value_set/v2026/encounter.py +2714 -0
- canvas_sdk/value_set/v2026/immunization.py +297 -0
- canvas_sdk/value_set/v2026/individual_characteristic.py +339 -0
- canvas_sdk/value_set/v2026/intervention.py +1703 -0
- canvas_sdk/value_set/v2026/laboratory_test.py +1831 -0
- canvas_sdk/value_set/v2026/medication.py +8218 -0
- canvas_sdk/value_set/v2026/no_qdm_category_assigned.py +26493 -0
- canvas_sdk/value_set/v2026/physical_exam.py +342 -0
- canvas_sdk/value_set/v2026/procedure.py +27869 -0
- canvas_sdk/value_set/v2026/symptom.py +625 -0
- logger/logger.py +30 -31
- logger/logstash.py +282 -0
- logger/pubsub.py +26 -0
- plugin_runner/allowed-module-imports.json +940 -9
- plugin_runner/generate_allowed_imports.py +1 -0
- plugin_runner/installation.py +2 -2
- plugin_runner/plugin_runner.py +21 -24
- plugin_runner/sandbox.py +34 -0
- protobufs/canvas_generated/messages/effects.proto +65 -0
- protobufs/canvas_generated/messages/events.proto +150 -51
- settings.py +27 -11
- canvas_sdk/effects/calendar/create_event.py +0 -43
- {canvas-0.63.0.dist-info → canvas-0.89.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from typing import Any, Literal
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from pydantic_core import InitErrorDetails
|
|
6
|
+
|
|
7
|
+
from canvas_sdk.effects import EffectType, _BaseEffect
|
|
8
|
+
from canvas_sdk.v1.data import PracticeLocation
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BaseFaxEffect(_BaseEffect, ABC):
|
|
12
|
+
"""Base class for fax effects."""
|
|
13
|
+
|
|
14
|
+
class Meta:
|
|
15
|
+
effect_type = EffectType.UNKNOWN_EFFECT
|
|
16
|
+
|
|
17
|
+
recipient_name: str
|
|
18
|
+
recipient_fax_number: str
|
|
19
|
+
include_coversheet: bool = False
|
|
20
|
+
subject: str | None = None
|
|
21
|
+
comment: str | None = None
|
|
22
|
+
location_id: str | UUID | None = None
|
|
23
|
+
|
|
24
|
+
def _get_error_details(self, method: Literal["apply"]) -> list[InitErrorDetails]:
|
|
25
|
+
errors = super()._get_error_details(method)
|
|
26
|
+
|
|
27
|
+
if self.include_coversheet:
|
|
28
|
+
if not self.subject:
|
|
29
|
+
errors.append(
|
|
30
|
+
self._create_error_detail(
|
|
31
|
+
"value",
|
|
32
|
+
"subject is required when include_coversheet is True",
|
|
33
|
+
self.subject,
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
if not self.comment:
|
|
37
|
+
errors.append(
|
|
38
|
+
self._create_error_detail(
|
|
39
|
+
"value",
|
|
40
|
+
"comment is required when include_coversheet is True",
|
|
41
|
+
self.comment,
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
if self.location_id:
|
|
45
|
+
if not PracticeLocation.objects.filter(id=self.location_id).exists():
|
|
46
|
+
errors.append(
|
|
47
|
+
self._create_error_detail(
|
|
48
|
+
"value",
|
|
49
|
+
f"Practice Location {self.location_id} does not exist",
|
|
50
|
+
self.location_id,
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
else:
|
|
54
|
+
errors.append(
|
|
55
|
+
self._create_error_detail(
|
|
56
|
+
"value",
|
|
57
|
+
"location_id is required when include_coversheet is True",
|
|
58
|
+
self.location_id,
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return errors
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def values(self) -> dict[str, Any]:
|
|
66
|
+
"""Return the values of the fax effect."""
|
|
67
|
+
return {
|
|
68
|
+
"recipient_name": self.recipient_name,
|
|
69
|
+
"recipient_fax_number": self.recipient_fax_number,
|
|
70
|
+
"include_coversheet": self.include_coversheet,
|
|
71
|
+
"subject": self.subject,
|
|
72
|
+
"comment": self.comment,
|
|
73
|
+
"location_id": str(self.location_id) if self.location_id else None,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
__exports__ = ()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
from uuid import UUID
|
|
3
|
+
|
|
4
|
+
from pydantic_core import InitErrorDetails
|
|
5
|
+
|
|
6
|
+
from canvas_generated.messages.effects_pb2 import EffectType
|
|
7
|
+
from canvas_sdk.effects.fax.base import BaseFaxEffect
|
|
8
|
+
from canvas_sdk.v1.data import Note
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FaxNoteEffect(BaseFaxEffect):
|
|
12
|
+
"""Fax note effect."""
|
|
13
|
+
|
|
14
|
+
class Meta:
|
|
15
|
+
effect_type = EffectType.FAX_NOTE
|
|
16
|
+
|
|
17
|
+
note_id: str | UUID
|
|
18
|
+
|
|
19
|
+
def _get_error_details(self, method: Literal["apply"]) -> list[InitErrorDetails]:
|
|
20
|
+
errors = super()._get_error_details(method)
|
|
21
|
+
if self.note_id and not Note.objects.filter(id=self.note_id).exists():
|
|
22
|
+
errors.append(
|
|
23
|
+
self._create_error_detail(
|
|
24
|
+
"value",
|
|
25
|
+
f"Note with ID {self.note_id} does not exist.",
|
|
26
|
+
self.note_id,
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
return errors
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def values(self) -> dict[str, str]:
|
|
34
|
+
"""Return the values of the note fax effect."""
|
|
35
|
+
values = super().values
|
|
36
|
+
return {
|
|
37
|
+
**values,
|
|
38
|
+
"note_id": str(self.note_id),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
__exports__ = ()
|
canvas_sdk/effects/metadata.py
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import json
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any
|
|
2
4
|
|
|
3
5
|
from canvas_generated.messages.effects_pb2 import Effect
|
|
4
6
|
from canvas_sdk.base import TrackableFieldsModel
|
|
5
7
|
|
|
6
8
|
|
|
9
|
+
@dataclass
|
|
10
|
+
class Metadata:
|
|
11
|
+
"""A class representing a metadata."""
|
|
12
|
+
|
|
13
|
+
key: str
|
|
14
|
+
value: str
|
|
15
|
+
|
|
16
|
+
def to_dict(self) -> dict[str, Any]:
|
|
17
|
+
"""Convert the metadata to a dictionary."""
|
|
18
|
+
return {"key": self.key, "value": self.value}
|
|
19
|
+
|
|
20
|
+
|
|
7
21
|
class BaseMetadata(TrackableFieldsModel):
|
|
8
22
|
"""Base class for metadata effects."""
|
|
9
23
|
|
|
@@ -23,4 +37,4 @@ class BaseMetadata(TrackableFieldsModel):
|
|
|
23
37
|
)
|
|
24
38
|
|
|
25
39
|
|
|
26
|
-
__exports__ = ()
|
|
40
|
+
__exports__ = ("Metadata",)
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
from canvas_sdk.effects.note.appointment import
|
|
1
|
+
from canvas_sdk.effects.note.appointment import (
|
|
2
|
+
AddAppointmentLabel,
|
|
3
|
+
Appointment,
|
|
4
|
+
RemoveAppointmentLabel,
|
|
5
|
+
ScheduleEvent,
|
|
6
|
+
)
|
|
2
7
|
from canvas_sdk.effects.note.base import AppointmentIdentifier
|
|
3
8
|
from canvas_sdk.effects.note.note import Note
|
|
4
9
|
|
|
@@ -7,4 +12,6 @@ __all__ = __exports__ = (
|
|
|
7
12
|
"Note",
|
|
8
13
|
"Appointment",
|
|
9
14
|
"ScheduleEvent",
|
|
15
|
+
"AddAppointmentLabel",
|
|
16
|
+
"RemoveAppointmentLabel",
|
|
10
17
|
)
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import json
|
|
2
|
-
from typing import Any
|
|
2
|
+
from typing import Annotated, Any
|
|
3
3
|
from uuid import UUID
|
|
4
4
|
|
|
5
|
+
from django.db.models import Count
|
|
6
|
+
from pydantic import Field
|
|
5
7
|
from pydantic_core import InitErrorDetails
|
|
6
8
|
|
|
7
|
-
from canvas_sdk.
|
|
9
|
+
from canvas_sdk.base import TrackableFieldsModel
|
|
10
|
+
from canvas_sdk.effects import Effect, EffectType
|
|
11
|
+
from canvas_sdk.effects.base import _BaseEffect
|
|
8
12
|
from canvas_sdk.effects.note.base import AppointmentABC
|
|
9
|
-
from canvas_sdk.v1.data import
|
|
13
|
+
from canvas_sdk.v1.data import Appointment as AppointmentDataModel
|
|
14
|
+
from canvas_sdk.v1.data import AppointmentLabel, NoteType, Patient
|
|
10
15
|
from canvas_sdk.v1.data.note import NoteTypeCategories
|
|
11
16
|
|
|
12
17
|
|
|
@@ -127,6 +132,7 @@ class Appointment(AppointmentABC):
|
|
|
127
132
|
appointment_note_type_id (UUID | str | None): The ID of the appointment note type.
|
|
128
133
|
meeting_link (str | None): The meeting link for the appointment, if any.
|
|
129
134
|
patient_id (str | None): The ID of the patient.
|
|
135
|
+
labels (conset[str] | None): A set of label names to apply to the appointment.
|
|
130
136
|
"""
|
|
131
137
|
|
|
132
138
|
class Meta:
|
|
@@ -135,6 +141,13 @@ class Appointment(AppointmentABC):
|
|
|
135
141
|
appointment_note_type_id: UUID | str | None = None
|
|
136
142
|
meeting_link: str | None = None
|
|
137
143
|
patient_id: str | None = None
|
|
144
|
+
labels: (
|
|
145
|
+
Annotated[
|
|
146
|
+
set[Annotated[str, Field(min_length=1, max_length=50)]],
|
|
147
|
+
Field(min_length=1, max_length=3),
|
|
148
|
+
]
|
|
149
|
+
| None
|
|
150
|
+
) = None
|
|
138
151
|
|
|
139
152
|
def _get_error_details(self, method: Any) -> list[InitErrorDetails]:
|
|
140
153
|
"""
|
|
@@ -186,9 +199,11 @@ class Appointment(AppointmentABC):
|
|
|
186
199
|
|
|
187
200
|
if self.appointment_note_type_id:
|
|
188
201
|
try:
|
|
189
|
-
category, is_scheduleable =
|
|
190
|
-
"category", "is_scheduleable"
|
|
191
|
-
|
|
202
|
+
category, is_scheduleable = (
|
|
203
|
+
NoteType.objects.values_list("category", "is_scheduleable")
|
|
204
|
+
.filter(is_active=True)
|
|
205
|
+
.get(id=self.appointment_note_type_id)
|
|
206
|
+
)
|
|
192
207
|
if category != NoteTypeCategories.ENCOUNTER:
|
|
193
208
|
errors.append(
|
|
194
209
|
self._create_error_detail(
|
|
@@ -215,8 +230,39 @@ class Appointment(AppointmentABC):
|
|
|
215
230
|
)
|
|
216
231
|
)
|
|
217
232
|
|
|
233
|
+
if method == "update" and self.labels and self.instance_id:
|
|
234
|
+
existing_count = AppointmentLabel.objects.filter(
|
|
235
|
+
appointment__id=self.instance_id
|
|
236
|
+
).count()
|
|
237
|
+
if existing_count + len(self.labels) > 3:
|
|
238
|
+
errors.append(
|
|
239
|
+
self._create_error_detail(
|
|
240
|
+
"value",
|
|
241
|
+
f"Limit reached: Only 3 appointment labels allowed. Attempted to add {len(self.labels)} label(s) to appointment with {existing_count} existing label(s).",
|
|
242
|
+
sorted(self.labels),
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
|
|
218
246
|
return errors
|
|
219
247
|
|
|
248
|
+
@property
|
|
249
|
+
def values(self) -> dict:
|
|
250
|
+
"""
|
|
251
|
+
Returns a dictionary of modified attributes with type-specific transformations.
|
|
252
|
+
"""
|
|
253
|
+
values = super().values
|
|
254
|
+
# Convert labels set to list for JSON serialization
|
|
255
|
+
# This is necessary because:
|
|
256
|
+
# 1. The labels field is defined as a conset (constrained set) for validation
|
|
257
|
+
# 2. JSON cannot serialize Python sets directly - it only supports lists, dicts, strings, numbers, booleans, and None
|
|
258
|
+
# 3. When the effect payload is serialized to JSON in the base Effect class, it would fail with:
|
|
259
|
+
# "TypeError: Object of type set is not JSON serializable"
|
|
260
|
+
# 4. Converting to list maintains the same data while making it JSON-compatible
|
|
261
|
+
# 5. Sort the labels to ensure consistent ordering for tests and API responses
|
|
262
|
+
if self.labels is not None:
|
|
263
|
+
values["labels"] = sorted(self.labels)
|
|
264
|
+
return values
|
|
265
|
+
|
|
220
266
|
def cancel(self) -> Effect:
|
|
221
267
|
"""Send a CANCEL effect for the appointment."""
|
|
222
268
|
self._validate_before_effect("cancel")
|
|
@@ -230,4 +276,86 @@ class Appointment(AppointmentABC):
|
|
|
230
276
|
)
|
|
231
277
|
|
|
232
278
|
|
|
233
|
-
|
|
279
|
+
class _AppointmentLabelBase(_BaseEffect, TrackableFieldsModel):
|
|
280
|
+
"""
|
|
281
|
+
Base class for appointment label effects.
|
|
282
|
+
|
|
283
|
+
Attributes:
|
|
284
|
+
appointment_id (UUID | str): The ID of the appointment.
|
|
285
|
+
labels (conset[str]): A set of label names (1-3 labels allowed).
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
appointment_id: str
|
|
289
|
+
labels: Annotated[set[str], Field(min_length=1, max_length=3)]
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def values(self) -> dict:
|
|
293
|
+
"""The effect's values."""
|
|
294
|
+
result = {
|
|
295
|
+
"appointment_id": str(self.appointment_id),
|
|
296
|
+
"labels": sorted(self.labels),
|
|
297
|
+
}
|
|
298
|
+
return result
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class AddAppointmentLabel(_AppointmentLabelBase):
|
|
302
|
+
"""
|
|
303
|
+
Effect to add one or more labels to an appointment.
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
class Meta:
|
|
307
|
+
effect_type = EffectType.ADD_APPOINTMENT_LABEL
|
|
308
|
+
|
|
309
|
+
def _get_error_details(self, method: Any) -> list[InitErrorDetails]:
|
|
310
|
+
"""Validate that the appointment does not exceed the 3-label limit."""
|
|
311
|
+
errors = super()._get_error_details(method)
|
|
312
|
+
|
|
313
|
+
appointment_label_count = (
|
|
314
|
+
AppointmentDataModel.objects.filter(id=self.appointment_id)
|
|
315
|
+
.annotate(label_count=Count("labels"))
|
|
316
|
+
.values_list("label_count", flat=True)
|
|
317
|
+
.first()
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# note that appointment_label_count will be None if the appointment doesn't exist
|
|
321
|
+
# and appointment_label_count will be 0 if the appointment exists but has no labels
|
|
322
|
+
if appointment_label_count is None:
|
|
323
|
+
errors.append(
|
|
324
|
+
self._create_error_detail(
|
|
325
|
+
"value",
|
|
326
|
+
f"Appointment {self.appointment_id} does not exist",
|
|
327
|
+
self.appointment_id,
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
elif appointment_label_count + len(self.labels) > 3:
|
|
331
|
+
errors.append(
|
|
332
|
+
self._create_error_detail(
|
|
333
|
+
"value",
|
|
334
|
+
f"Limit reached: Only 3 appointment labels allowed. "
|
|
335
|
+
f"Attempted to add {len(self.labels)} label(s) to appointment with "
|
|
336
|
+
f"{appointment_label_count} existing label(s).",
|
|
337
|
+
sorted(self.labels),
|
|
338
|
+
)
|
|
339
|
+
)
|
|
340
|
+
return errors
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class RemoveAppointmentLabel(_AppointmentLabelBase):
|
|
344
|
+
"""
|
|
345
|
+
Effect to remove one or more labels from an appointment.
|
|
346
|
+
|
|
347
|
+
Attributes:
|
|
348
|
+
appointment_id (UUID | str): The ID of the appointment to remove labels from.
|
|
349
|
+
labels (list[str]): A list of label names to remove.
|
|
350
|
+
"""
|
|
351
|
+
|
|
352
|
+
class Meta:
|
|
353
|
+
effect_type = EffectType.REMOVE_APPOINTMENT_LABEL
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
__exports__ = (
|
|
357
|
+
"ScheduleEvent",
|
|
358
|
+
"Appointment",
|
|
359
|
+
"AddAppointmentLabel",
|
|
360
|
+
"RemoveAppointmentLabel",
|
|
361
|
+
)
|
canvas_sdk/effects/note/base.py
CHANGED
|
@@ -239,5 +239,22 @@ class AppointmentABC(NoteOrAppointmentABC, ABC):
|
|
|
239
239
|
|
|
240
240
|
return values
|
|
241
241
|
|
|
242
|
+
def reschedule(self) -> Effect:
|
|
243
|
+
"""Send a RESCHEDULE effect for the appointment."""
|
|
244
|
+
self._validate_before_effect("update")
|
|
245
|
+
|
|
246
|
+
# Check if any fields were actually modified
|
|
247
|
+
if self._dirty_keys == {"instance_id"}:
|
|
248
|
+
raise ValueError("No fields have been modified. Nothing to update.")
|
|
249
|
+
|
|
250
|
+
return Effect(
|
|
251
|
+
type=f"RESCHEDULE_{self.Meta.effect_type}",
|
|
252
|
+
payload=json.dumps(
|
|
253
|
+
{
|
|
254
|
+
"data": self.values,
|
|
255
|
+
}
|
|
256
|
+
),
|
|
257
|
+
)
|
|
258
|
+
|
|
242
259
|
|
|
243
260
|
__exports__ = ("AppointmentIdentifier", "NoteOrAppointmentABC", "AppointmentABC")
|
|
@@ -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/effects/note/note.py
CHANGED
|
@@ -1,13 +1,33 @@
|
|
|
1
1
|
import datetime
|
|
2
|
+
import json
|
|
2
3
|
from typing import Any
|
|
3
4
|
from uuid import UUID
|
|
4
5
|
|
|
5
6
|
from pydantic_core import InitErrorDetails
|
|
6
7
|
|
|
8
|
+
from canvas_generated.messages.effects_pb2 import EffectType
|
|
9
|
+
from canvas_sdk.effects import Effect
|
|
7
10
|
from canvas_sdk.effects.note.base import NoteOrAppointmentABC
|
|
8
11
|
from canvas_sdk.v1.data import Note as NoteModel
|
|
9
12
|
from canvas_sdk.v1.data import NoteType, Patient
|
|
10
|
-
from canvas_sdk.v1.data.note import NoteTypeCategories
|
|
13
|
+
from canvas_sdk.v1.data.note import NoteStates, NoteTypeCategories
|
|
14
|
+
|
|
15
|
+
TRANSITION_STATE_MATRIX = {
|
|
16
|
+
NoteStates.NEW: [NoteStates.LOCKED, NoteStates.PUSHED],
|
|
17
|
+
NoteStates.LOCKED: [NoteStates.SIGNED, NoteStates.UNLOCKED],
|
|
18
|
+
NoteStates.UNLOCKED: [NoteStates.LOCKED, NoteStates.PUSHED],
|
|
19
|
+
NoteStates.SIGNED: [NoteStates.UNLOCKED, NoteStates.SIGNED],
|
|
20
|
+
NoteStates.PUSHED: [NoteStates.LOCKED, NoteStates.PUSHED],
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
ACTION_STATE_MATRIX = {
|
|
24
|
+
"lock": NoteStates.LOCKED,
|
|
25
|
+
"unlock": NoteStates.UNLOCKED,
|
|
26
|
+
"sign": NoteStates.SIGNED,
|
|
27
|
+
"push_charges": NoteStates.PUSHED,
|
|
28
|
+
"check_in": NoteStates.CONVERTED,
|
|
29
|
+
"no_show": NoteStates.NOSHOW,
|
|
30
|
+
}
|
|
11
31
|
|
|
12
32
|
|
|
13
33
|
class Note(NoteOrAppointmentABC):
|
|
@@ -28,6 +48,82 @@ class Note(NoteOrAppointmentABC):
|
|
|
28
48
|
patient_id: str | None = None
|
|
29
49
|
title: str | None = None
|
|
30
50
|
|
|
51
|
+
def push_charges(self) -> Effect:
|
|
52
|
+
"""Pushes BillingLineItems from the Note to the associated Claim. Identicial to clicking the Push Charges button in the note footer."""
|
|
53
|
+
self._validate_before_effect("push_charges")
|
|
54
|
+
return Effect(
|
|
55
|
+
type=EffectType.PUSH_NOTE_CHARGES,
|
|
56
|
+
payload=json.dumps({"data": {"note": str(self.instance_id)}}),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def unlock(self) -> Effect:
|
|
60
|
+
"""Unlocks the note to allow further edits."""
|
|
61
|
+
self._validate_before_effect("unlock")
|
|
62
|
+
return Effect(
|
|
63
|
+
type=EffectType.UNLOCK_NOTE,
|
|
64
|
+
payload=json.dumps({"data": {"note": str(self.instance_id)}}),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def lock(self) -> Effect:
|
|
68
|
+
"""Locks the note to prevent further edits."""
|
|
69
|
+
self._validate_before_effect("lock")
|
|
70
|
+
return Effect(
|
|
71
|
+
type=EffectType.LOCK_NOTE,
|
|
72
|
+
payload=json.dumps({"data": {"note": str(self.instance_id)}}),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def sign(self) -> Effect:
|
|
76
|
+
"""Signs the note."""
|
|
77
|
+
self._validate_before_effect("sign")
|
|
78
|
+
return Effect(
|
|
79
|
+
type=EffectType.SIGN_NOTE,
|
|
80
|
+
payload=json.dumps({"data": {"note": str(self.instance_id)}}),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def check_in(self) -> Effect:
|
|
84
|
+
"""Mark the note as checked-in."""
|
|
85
|
+
self._validate_before_effect("check_in")
|
|
86
|
+
return Effect(
|
|
87
|
+
type=EffectType.CHECK_IN_NOTE,
|
|
88
|
+
payload=json.dumps({"data": {"note": str(self.instance_id)}}),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def no_show(self) -> Effect:
|
|
92
|
+
"""Mark the note as no-show."""
|
|
93
|
+
self._validate_before_effect("no_show")
|
|
94
|
+
return Effect(
|
|
95
|
+
type=EffectType.NO_SHOW_NOTE,
|
|
96
|
+
payload=json.dumps({"data": {"note": str(self.instance_id)}}),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def _validate_state_transition(
|
|
100
|
+
self, note: NoteModel, next_state: NoteStates
|
|
101
|
+
) -> tuple[bool, InitErrorDetails | None]:
|
|
102
|
+
"""Validates state transitions for the note."""
|
|
103
|
+
current_state = note.current_state.state if note.current_state else None
|
|
104
|
+
|
|
105
|
+
if not current_state:
|
|
106
|
+
return False, self._create_error_detail(
|
|
107
|
+
"value", "Unsupported state transitions", next_state
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
is_sig_required = note.note_type_version.is_sig_required
|
|
111
|
+
if next_state == NoteStates.SIGNED and not is_sig_required:
|
|
112
|
+
return False, self._create_error_detail(
|
|
113
|
+
"value", "Cannot sign a note that does not require a signature.", next_state
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if (
|
|
117
|
+
next_state == NoteStates.CONVERTED or next_state == NoteStates.NOSHOW
|
|
118
|
+
) and note.note_type_version.category != NoteTypeCategories.APPOINTMENT:
|
|
119
|
+
return False, self._create_error_detail(
|
|
120
|
+
"value",
|
|
121
|
+
"Only appointments can be checked in or marked as no-show.",
|
|
122
|
+
next_state,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return True, None
|
|
126
|
+
|
|
31
127
|
def _get_error_details(self, method: Any) -> list[InitErrorDetails]:
|
|
32
128
|
"""
|
|
33
129
|
Validates the note type category and returns a list of error details if validation fails.
|
|
@@ -40,6 +136,59 @@ class Note(NoteOrAppointmentABC):
|
|
|
40
136
|
"""
|
|
41
137
|
errors = super()._get_error_details(method)
|
|
42
138
|
|
|
139
|
+
if method in ACTION_STATE_MATRIX:
|
|
140
|
+
if not self.instance_id:
|
|
141
|
+
errors.append(
|
|
142
|
+
self._create_error_detail(
|
|
143
|
+
"missing",
|
|
144
|
+
"Field 'instance_id' is required.",
|
|
145
|
+
None,
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
return errors
|
|
149
|
+
elif not (note := NoteModel.objects.filter(id=self.instance_id).first()):
|
|
150
|
+
errors.append(
|
|
151
|
+
self._create_error_detail(
|
|
152
|
+
"value",
|
|
153
|
+
f"Note with ID {self.instance_id} does not exist.",
|
|
154
|
+
self.instance_id,
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
return errors
|
|
158
|
+
|
|
159
|
+
if note.note_type_version.category in (
|
|
160
|
+
NoteTypeCategories.LETTER,
|
|
161
|
+
NoteTypeCategories.MESSAGE,
|
|
162
|
+
):
|
|
163
|
+
errors.append(
|
|
164
|
+
self._create_error_detail(
|
|
165
|
+
"value",
|
|
166
|
+
f"Note with note type {note.note_type_version.name} cannot perform action '{method}'.",
|
|
167
|
+
note.note_type_version,
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
return errors
|
|
171
|
+
|
|
172
|
+
if method == "push_charges" and (
|
|
173
|
+
not note.note_type_version or not note.note_type_version.is_billable
|
|
174
|
+
):
|
|
175
|
+
errors.append(
|
|
176
|
+
self._create_error_detail(
|
|
177
|
+
"value",
|
|
178
|
+
f"Note with note type {note.note_type_version} is not billable and has no associated claim.",
|
|
179
|
+
note.note_type_version,
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
return errors
|
|
183
|
+
|
|
184
|
+
if method in ACTION_STATE_MATRIX:
|
|
185
|
+
_, error = self._validate_state_transition(note, ACTION_STATE_MATRIX[method])
|
|
186
|
+
|
|
187
|
+
if error:
|
|
188
|
+
errors.append(error)
|
|
189
|
+
|
|
190
|
+
return errors
|
|
191
|
+
|
|
43
192
|
if method == "create":
|
|
44
193
|
if not self.note_type_id:
|
|
45
194
|
errors.append(
|