canvas 0.19.0__py3-none-any.whl → 0.20.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.19.0.dist-info → canvas-0.20.0.dist-info}/METADATA +1 -1
- {canvas-0.19.0.dist-info → canvas-0.20.0.dist-info}/RECORD +48 -47
- canvas_generated/messages/effects_pb2.py +2 -2
- canvas_generated/messages/effects_pb2.pyi +10 -0
- canvas_generated/messages/events_pb2.py +2 -2
- canvas_generated/messages/events_pb2.pyi +32 -0
- canvas_sdk/__init__.py +4 -2
- canvas_sdk/commands/__init__.py +2 -0
- canvas_sdk/commands/base.py +47 -3
- canvas_sdk/commands/commands/allergy.py +0 -12
- canvas_sdk/commands/commands/assess.py +0 -10
- canvas_sdk/commands/commands/close_goal.py +0 -11
- canvas_sdk/commands/commands/diagnose.py +0 -14
- canvas_sdk/commands/commands/family_history.py +0 -5
- canvas_sdk/commands/commands/follow_up.py +69 -0
- canvas_sdk/commands/commands/goal.py +0 -14
- canvas_sdk/commands/commands/history_present_illness.py +0 -5
- canvas_sdk/commands/commands/instruct.py +0 -5
- canvas_sdk/commands/commands/lab_order.py +59 -11
- canvas_sdk/commands/commands/medical_history.py +0 -15
- canvas_sdk/commands/commands/medication_statement.py +0 -5
- canvas_sdk/commands/commands/past_surgical_history.py +0 -11
- canvas_sdk/commands/commands/perform.py +0 -5
- canvas_sdk/commands/commands/plan.py +0 -5
- canvas_sdk/commands/commands/prescribe.py +7 -14
- canvas_sdk/commands/commands/questionnaire.py +0 -5
- canvas_sdk/commands/commands/reason_for_visit.py +0 -9
- canvas_sdk/commands/commands/remove_allergy.py +0 -8
- canvas_sdk/commands/commands/stop_medication.py +0 -5
- canvas_sdk/commands/commands/task.py +0 -12
- canvas_sdk/commands/commands/update_diagnosis.py +0 -10
- canvas_sdk/commands/commands/update_goal.py +0 -13
- canvas_sdk/commands/commands/vitals.py +0 -26
- canvas_sdk/commands/tests/test_base_command.py +81 -0
- canvas_sdk/effects/launch_modal.py +1 -0
- canvas_sdk/utils/stats.py +35 -0
- canvas_sdk/v1/data/lab.py +38 -0
- canvas_sdk/v1/data/note.py +3 -0
- logger/logger.py +8 -1
- plugin_runner/{plugin_installer.py → installation.py} +46 -43
- plugin_runner/plugin_runner.py +70 -40
- plugin_runner/tests/test_plugin_installer.py +3 -3
- plugin_runner/tests/test_plugin_runner.py +1 -1
- protobufs/canvas_generated/messages/effects.proto +6 -0
- protobufs/canvas_generated/messages/events.proto +16 -12
- settings.py +22 -12
- canvas_sdk/utils/db.py +0 -17
- {canvas-0.19.0.dist-info → canvas-0.20.0.dist-info}/WHEEL +0 -0
- {canvas-0.19.0.dist-info → canvas-0.20.0.dist-info}/entry_points.txt +0 -0
canvas_sdk/commands/base.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import datetime
|
|
1
2
|
import json
|
|
2
3
|
import re
|
|
3
|
-
from enum import EnumType
|
|
4
|
+
from enum import Enum, EnumType
|
|
4
5
|
from types import NoneType, UnionType
|
|
5
|
-
from typing import Union, get_args, get_origin
|
|
6
|
+
from typing import Any, Union, get_args, get_origin
|
|
7
|
+
from uuid import UUID
|
|
6
8
|
|
|
7
9
|
from canvas_sdk.base import Model
|
|
8
10
|
from canvas_sdk.commands.constants import Coding
|
|
@@ -19,6 +21,32 @@ class _BaseCommand(Model):
|
|
|
19
21
|
commit_required_fields = ("command_uuid",)
|
|
20
22
|
enter_in_error_required_fields = ("command_uuid",)
|
|
21
23
|
|
|
24
|
+
# A set to track which fields have been modified.
|
|
25
|
+
_dirty_keys: set[str] = set()
|
|
26
|
+
|
|
27
|
+
def __init__(self, /, **data: Any) -> None:
|
|
28
|
+
"""Initialize the command and mark all provided keys as dirty."""
|
|
29
|
+
super().__init__(**data)
|
|
30
|
+
|
|
31
|
+
# Initialize a set to track which fields have been modified.
|
|
32
|
+
self._dirty_keys = set()
|
|
33
|
+
|
|
34
|
+
# Explicitly mark all keys provided in the constructor as dirty.
|
|
35
|
+
self._dirty_keys.update(data.keys())
|
|
36
|
+
|
|
37
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
38
|
+
"""Set an attribute and mark it as dirty unless excluded."""
|
|
39
|
+
if not name.startswith("_") and name not in (
|
|
40
|
+
"note_uuid",
|
|
41
|
+
"command_uuid",
|
|
42
|
+
):
|
|
43
|
+
self._dirty_keys.add(name)
|
|
44
|
+
super().__setattr__(name, value)
|
|
45
|
+
|
|
46
|
+
def is_dirty(self, key: str) -> bool:
|
|
47
|
+
"""Returns True if the given property has been modified (i.e. marked as dirty), False otherwise."""
|
|
48
|
+
return key in self._dirty_keys
|
|
49
|
+
|
|
22
50
|
def constantized_key(self) -> str:
|
|
23
51
|
return re.sub(r"(?<!^)(?=[A-Z])", "_", self.Meta.key).upper()
|
|
24
52
|
|
|
@@ -32,7 +60,23 @@ class _BaseCommand(Model):
|
|
|
32
60
|
|
|
33
61
|
@property
|
|
34
62
|
def values(self) -> dict:
|
|
35
|
-
|
|
63
|
+
"""Return a dictionary of modified attributes with type-specific transformations."""
|
|
64
|
+
result = {}
|
|
65
|
+
for key in self._dirty_keys:
|
|
66
|
+
value = getattr(self, key)
|
|
67
|
+
if isinstance(value, Enum):
|
|
68
|
+
# If it's an enum, use its .value.
|
|
69
|
+
result[key] = value.value if value else None
|
|
70
|
+
elif isinstance(value, datetime.date | datetime.datetime):
|
|
71
|
+
# If it's a date/datetime, use isoformat().
|
|
72
|
+
result[key] = value.isoformat() if value else None
|
|
73
|
+
elif isinstance(value, UUID):
|
|
74
|
+
# If it's a UUID, use its string representation.
|
|
75
|
+
result[key] = str(value) if value else None
|
|
76
|
+
else:
|
|
77
|
+
# For strings, integers, or any other type, return as is.
|
|
78
|
+
result[key] = value
|
|
79
|
+
return result
|
|
36
80
|
|
|
37
81
|
@property
|
|
38
82
|
def coding_filter(self) -> Coding | None:
|
|
@@ -37,15 +37,3 @@ class AllergyCommand(BaseCommand):
|
|
|
37
37
|
severity: Severity | None = None
|
|
38
38
|
narrative: str | None = None
|
|
39
39
|
approximate_date: date | None = None
|
|
40
|
-
|
|
41
|
-
@property
|
|
42
|
-
def values(self) -> dict:
|
|
43
|
-
"""The Allergy command's field values."""
|
|
44
|
-
return {
|
|
45
|
-
"allergy": self.allergy,
|
|
46
|
-
"severity": self.severity.value if self.severity else None,
|
|
47
|
-
"narrative": self.narrative,
|
|
48
|
-
"approximate_date": (
|
|
49
|
-
self.approximate_date.isoformat() if self.approximate_date else None
|
|
50
|
-
),
|
|
51
|
-
}
|
|
@@ -24,16 +24,6 @@ class AssessCommand(_BaseCommand):
|
|
|
24
24
|
status: Status | None = None
|
|
25
25
|
narrative: str | None = None
|
|
26
26
|
|
|
27
|
-
@property
|
|
28
|
-
def values(self) -> dict:
|
|
29
|
-
"""The Assess command's field values."""
|
|
30
|
-
return {
|
|
31
|
-
"condition_id": self.condition_id,
|
|
32
|
-
"background": self.background,
|
|
33
|
-
"status": self.status.value if self.status else None,
|
|
34
|
-
"narrative": self.narrative,
|
|
35
|
-
}
|
|
36
|
-
|
|
37
27
|
|
|
38
28
|
# how do we make sure that condition_id is a valid condition for the patient?
|
|
39
29
|
|
|
@@ -12,14 +12,3 @@ class CloseGoalCommand(BaseCommand):
|
|
|
12
12
|
goal_id: int | None = None
|
|
13
13
|
achievement_status: GoalCommand.AchievementStatus | None = None
|
|
14
14
|
progress: str | None = None
|
|
15
|
-
|
|
16
|
-
@property
|
|
17
|
-
def values(self) -> dict:
|
|
18
|
-
"""The CloseGoal command's field values."""
|
|
19
|
-
return {
|
|
20
|
-
"goal_id": self.goal_id,
|
|
21
|
-
"achievement_status": (
|
|
22
|
-
self.achievement_status.value if self.achievement_status else None
|
|
23
|
-
),
|
|
24
|
-
"progress": self.progress,
|
|
25
|
-
}
|
|
@@ -18,17 +18,3 @@ class DiagnoseCommand(_BaseCommand):
|
|
|
18
18
|
background: str | None = None
|
|
19
19
|
approximate_date_of_onset: date | None = None
|
|
20
20
|
today_assessment: str | None = None
|
|
21
|
-
|
|
22
|
-
@property
|
|
23
|
-
def values(self) -> dict:
|
|
24
|
-
"""The Diagnose command's field values."""
|
|
25
|
-
return {
|
|
26
|
-
"icd10_code": self.icd10_code,
|
|
27
|
-
"background": self.background,
|
|
28
|
-
"approximate_date_of_onset": (
|
|
29
|
-
self.approximate_date_of_onset.isoformat()
|
|
30
|
-
if self.approximate_date_of_onset
|
|
31
|
-
else None
|
|
32
|
-
),
|
|
33
|
-
"today_assessment": self.today_assessment,
|
|
34
|
-
}
|
|
@@ -11,8 +11,3 @@ class FamilyHistoryCommand(BaseCommand):
|
|
|
11
11
|
family_history: str | None = None
|
|
12
12
|
relative: str | None = None
|
|
13
13
|
note: str | None = None
|
|
14
|
-
|
|
15
|
-
@property
|
|
16
|
-
def values(self) -> dict:
|
|
17
|
-
"""The Family History command's field values."""
|
|
18
|
-
return {"family_history": self.family_history, "relative": self.relative, "note": self.note}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
from typing import Literal
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from pydantic_core import InitErrorDetails
|
|
6
|
+
|
|
7
|
+
from canvas_sdk.commands.base import _BaseCommand
|
|
8
|
+
from canvas_sdk.commands.constants import Coding
|
|
9
|
+
from canvas_sdk.v1.data import NoteType, ReasonForVisitSettingCoding
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FollowUpCommand(_BaseCommand):
|
|
13
|
+
"""A class for managing a Follow-Up command within a specific note."""
|
|
14
|
+
|
|
15
|
+
class Meta:
|
|
16
|
+
key = "followUp"
|
|
17
|
+
commit_required_fields = ("requested_date", "note_type")
|
|
18
|
+
|
|
19
|
+
structured: bool = False
|
|
20
|
+
requested_date: date | None = None
|
|
21
|
+
note_type_id: UUID | str | None = None
|
|
22
|
+
reason_for_visit: Coding | UUID | str | None = None
|
|
23
|
+
comment: str | None = None
|
|
24
|
+
|
|
25
|
+
def _get_error_details(
|
|
26
|
+
self, method: Literal["originate", "edit", "delete", "commit", "enter_in_error"]
|
|
27
|
+
) -> list[InitErrorDetails]:
|
|
28
|
+
errors = super()._get_error_details(method)
|
|
29
|
+
|
|
30
|
+
if self.reason_for_visit:
|
|
31
|
+
if self.structured:
|
|
32
|
+
if isinstance(self.reason_for_visit, str | UUID):
|
|
33
|
+
query = {"id": self.reason_for_visit}
|
|
34
|
+
error_message = f"ReasonForVisitSettingCoding with id {self.reason_for_visit} does not exist."
|
|
35
|
+
else:
|
|
36
|
+
query = {
|
|
37
|
+
"code": self.reason_for_visit["code"],
|
|
38
|
+
"system": self.reason_for_visit["system"],
|
|
39
|
+
}
|
|
40
|
+
error_message = f"ReasonForVisitSettingCoding with code {self.reason_for_visit['code']} and system {self.reason_for_visit['system']} does not exist."
|
|
41
|
+
|
|
42
|
+
if not ReasonForVisitSettingCoding.objects.filter(**query).exists():
|
|
43
|
+
errors.append(
|
|
44
|
+
self._create_error_detail("value", error_message, self.reason_for_visit)
|
|
45
|
+
)
|
|
46
|
+
elif not isinstance(self.reason_for_visit, str):
|
|
47
|
+
errors.append(
|
|
48
|
+
self._create_error_detail(
|
|
49
|
+
"value",
|
|
50
|
+
"reason for visit must be a string when structured is False.",
|
|
51
|
+
self.reason_for_visit,
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if (
|
|
56
|
+
self.note_type_id
|
|
57
|
+
and not NoteType.objects.filter(
|
|
58
|
+
id=self.note_type_id, is_active=True, is_scheduleable=True
|
|
59
|
+
).exists()
|
|
60
|
+
):
|
|
61
|
+
errors.append(
|
|
62
|
+
self._create_error_detail(
|
|
63
|
+
"value",
|
|
64
|
+
f"NoteType with id {self.note_type_id} does not exist or is not scheduleable.",
|
|
65
|
+
self.note_type_id,
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return errors
|
|
@@ -33,17 +33,3 @@ class GoalCommand(_BaseCommand):
|
|
|
33
33
|
achievement_status: AchievementStatus | None = None
|
|
34
34
|
priority: Priority | None = None
|
|
35
35
|
progress: str | None = None
|
|
36
|
-
|
|
37
|
-
@property
|
|
38
|
-
def values(self) -> dict:
|
|
39
|
-
"""The Goal command's field values."""
|
|
40
|
-
return {
|
|
41
|
-
"goal_statement": self.goal_statement,
|
|
42
|
-
"start_date": (self.start_date.isoformat() if self.start_date else None),
|
|
43
|
-
"due_date": (self.due_date.isoformat() if self.due_date else None),
|
|
44
|
-
"achievement_status": (
|
|
45
|
-
self.achievement_status.value if self.achievement_status else None
|
|
46
|
-
),
|
|
47
|
-
"priority": (self.priority.value if self.priority else None),
|
|
48
|
-
"progress": self.progress,
|
|
49
|
-
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
from django.db.models.query_utils import Q
|
|
4
|
+
from pydantic_core import InitErrorDetails
|
|
5
|
+
|
|
1
6
|
from canvas_sdk.commands.base import _BaseCommand as BaseCommand
|
|
7
|
+
from canvas_sdk.v1.data.lab import LabPartner, LabPartnerTest
|
|
2
8
|
|
|
3
9
|
|
|
4
10
|
class LabOrderCommand(BaseCommand):
|
|
@@ -20,14 +26,56 @@ class LabOrderCommand(BaseCommand):
|
|
|
20
26
|
fasting_required: bool = False
|
|
21
27
|
comment: str | None = None
|
|
22
28
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
def _get_error_details(
|
|
30
|
+
self, method: Literal["originate", "edit", "delete", "commit", "enter_in_error"]
|
|
31
|
+
) -> list[InitErrorDetails]:
|
|
32
|
+
errors = super()._get_error_details(method)
|
|
33
|
+
|
|
34
|
+
lab_partner_obj = None
|
|
35
|
+
if self.lab_partner:
|
|
36
|
+
lab_partner_obj = (
|
|
37
|
+
LabPartner.objects.filter(Q(name=self.lab_partner) | Q(id=self.lab_partner))
|
|
38
|
+
.values("id", "dbid")
|
|
39
|
+
.first()
|
|
40
|
+
)
|
|
41
|
+
if not lab_partner_obj:
|
|
42
|
+
errors.append(
|
|
43
|
+
self._create_error_detail(
|
|
44
|
+
"value",
|
|
45
|
+
f"lab partner with Id or Name {self.lab_partner} not found",
|
|
46
|
+
self.lab_partner,
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if self.tests_order_codes:
|
|
51
|
+
if not lab_partner_obj:
|
|
52
|
+
errors.append(
|
|
53
|
+
self._create_error_detail(
|
|
54
|
+
"value", "lab partner is required to find tests", self.tests_order_codes
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
else:
|
|
58
|
+
tests = LabPartnerTest.objects.filter(
|
|
59
|
+
Q(order_code__in=self.tests_order_codes) | Q(id__in=self.tests_order_codes),
|
|
60
|
+
lab_partner_id=lab_partner_obj["dbid"],
|
|
61
|
+
).values_list("id", "order_code")
|
|
62
|
+
|
|
63
|
+
if tests.count() != len(self.tests_order_codes):
|
|
64
|
+
missing_tests = [
|
|
65
|
+
test
|
|
66
|
+
for test in self.tests_order_codes
|
|
67
|
+
if not any(
|
|
68
|
+
test == str(test_id) or test == order_code
|
|
69
|
+
for test_id, order_code in tests
|
|
70
|
+
)
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
errors.append(
|
|
74
|
+
self._create_error_detail(
|
|
75
|
+
"value",
|
|
76
|
+
f"tests with order codes {missing_tests} not found",
|
|
77
|
+
self.tests_order_codes,
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return errors
|
|
@@ -17,18 +17,3 @@ class MedicalHistoryCommand(BaseCommand):
|
|
|
17
17
|
approximate_end_date: date | None = None
|
|
18
18
|
show_on_condition_list: bool = True
|
|
19
19
|
comments: str | None = Field(max_length=1000, default=None)
|
|
20
|
-
|
|
21
|
-
@property
|
|
22
|
-
def values(self) -> dict:
|
|
23
|
-
"""The Medical History command's field values."""
|
|
24
|
-
return {
|
|
25
|
-
"past_medical_history": self.past_medical_history,
|
|
26
|
-
"approximate_start_date": (
|
|
27
|
-
self.approximate_start_date.isoformat() if self.approximate_start_date else None
|
|
28
|
-
),
|
|
29
|
-
"approximate_end_date": (
|
|
30
|
-
self.approximate_end_date.isoformat() if self.approximate_end_date else None
|
|
31
|
-
),
|
|
32
|
-
"show_on_condition_list": self.show_on_condition_list,
|
|
33
|
-
"comments": self.comments,
|
|
34
|
-
}
|
|
@@ -15,11 +15,6 @@ class MedicationStatementCommand(_BaseCommand):
|
|
|
15
15
|
)
|
|
16
16
|
sig: str | None = None
|
|
17
17
|
|
|
18
|
-
@property
|
|
19
|
-
def values(self) -> dict:
|
|
20
|
-
"""The MedicationStatement command's field values."""
|
|
21
|
-
return {"fdb_code": self.fdb_code, "sig": self.sig}
|
|
22
|
-
|
|
23
18
|
|
|
24
19
|
# how do we make sure fdb_code is a valid code?
|
|
25
20
|
|
|
@@ -15,14 +15,3 @@ class PastSurgicalHistoryCommand(BaseCommand):
|
|
|
15
15
|
past_surgical_history: str | None = None
|
|
16
16
|
approximate_date: date | None = None
|
|
17
17
|
comment: str | None = Field(max_length=1000, default=None)
|
|
18
|
-
|
|
19
|
-
@property
|
|
20
|
-
def values(self) -> dict:
|
|
21
|
-
"""The Past Surgical History command's field values."""
|
|
22
|
-
return {
|
|
23
|
-
"past_surgical_history": self.past_surgical_history,
|
|
24
|
-
"approximate_date": (
|
|
25
|
-
self.approximate_date.isoformat() if self.approximate_date else None
|
|
26
|
-
),
|
|
27
|
-
"comment": self.comment,
|
|
28
|
-
}
|
|
@@ -45,18 +45,11 @@ class PrescribeCommand(_BaseCommand):
|
|
|
45
45
|
@property
|
|
46
46
|
def values(self) -> dict:
|
|
47
47
|
"""The Prescribe command's field values."""
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
"days_supply": self.days_supply,
|
|
53
|
-
"quantity_to_dispense": (
|
|
48
|
+
values = super().values
|
|
49
|
+
|
|
50
|
+
if self.is_dirty("quantity_to_dispense"):
|
|
51
|
+
values["quantity_to_dispense"] = (
|
|
54
52
|
str(Decimal(self.quantity_to_dispense)) if self.quantity_to_dispense else None
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"substitutions": self.substitutions.value if self.substitutions else None,
|
|
59
|
-
"pharmacy": self.pharmacy,
|
|
60
|
-
"prescriber_id": self.prescriber_id,
|
|
61
|
-
"note_to_pharmacist": self.note_to_pharmacist,
|
|
62
|
-
}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return values
|
|
@@ -14,8 +14,3 @@ class QuestionnaireCommand(_BaseCommand):
|
|
|
14
14
|
default=None, json_schema_extra={"commands_api_name": "questionnaire"}
|
|
15
15
|
)
|
|
16
16
|
result: str | None = None
|
|
17
|
-
|
|
18
|
-
@property
|
|
19
|
-
def values(self) -> dict:
|
|
20
|
-
"""The Questionnaire command's field values."""
|
|
21
|
-
return {"questionnaire_id": self.questionnaire_id, "result": self.result}
|
|
@@ -47,12 +47,3 @@ class ReasonForVisitCommand(_BaseCommand):
|
|
|
47
47
|
# the commands api does not include the 'structured' field in the fields response
|
|
48
48
|
command_schema.pop("structured")
|
|
49
49
|
return command_schema
|
|
50
|
-
|
|
51
|
-
@property
|
|
52
|
-
def values(self) -> dict:
|
|
53
|
-
"""The ReasonForVisit command's field values."""
|
|
54
|
-
return {
|
|
55
|
-
"structured": self.structured,
|
|
56
|
-
"coding": str(self.coding) if isinstance(self.coding, UUID) else self.coding,
|
|
57
|
-
"comment": self.comment,
|
|
58
|
-
}
|
|
@@ -16,11 +16,3 @@ class RemoveAllergyCommand(BaseCommand):
|
|
|
16
16
|
json_schema_extra={"commands_api_name": "allergy"},
|
|
17
17
|
)
|
|
18
18
|
narrative: str | None = None
|
|
19
|
-
|
|
20
|
-
@property
|
|
21
|
-
def values(self) -> dict:
|
|
22
|
-
"""The Remove Allergy command's field values."""
|
|
23
|
-
return {
|
|
24
|
-
"allergy_id": self.allergy_id,
|
|
25
|
-
"narrative": self.narrative,
|
|
26
|
-
}
|
|
@@ -15,8 +15,3 @@ class StopMedicationCommand(_BaseCommand):
|
|
|
15
15
|
default=None, json_schema_extra={"commands_api_name": "medication"}
|
|
16
16
|
)
|
|
17
17
|
rationale: str | None = None
|
|
18
|
-
|
|
19
|
-
@property
|
|
20
|
-
def values(self) -> dict:
|
|
21
|
-
"""The StopMedication command's field values."""
|
|
22
|
-
return {"medication_id": self.medication_id, "rationale": self.rationale}
|
|
@@ -39,15 +39,3 @@ class TaskCommand(BaseCommand):
|
|
|
39
39
|
comment: str | None = None
|
|
40
40
|
labels: list[str] | None = None
|
|
41
41
|
linked_items_urns: list[str] | None = None
|
|
42
|
-
|
|
43
|
-
@property
|
|
44
|
-
def values(self) -> dict:
|
|
45
|
-
"""The Task command's field values."""
|
|
46
|
-
return {
|
|
47
|
-
"title": self.title,
|
|
48
|
-
"assign_to": self.assign_to,
|
|
49
|
-
"due_date": self.due_date.isoformat() if self.due_date else None,
|
|
50
|
-
"comment": self.comment,
|
|
51
|
-
"labels": self.labels,
|
|
52
|
-
"linked_items_urns": self.linked_items_urns,
|
|
53
|
-
}
|
|
@@ -15,13 +15,3 @@ class UpdateDiagnosisCommand(BaseCommand):
|
|
|
15
15
|
new_condition_code: str | None = None
|
|
16
16
|
background: str | None = None
|
|
17
17
|
narrative: str | None = None
|
|
18
|
-
|
|
19
|
-
@property
|
|
20
|
-
def values(self) -> dict:
|
|
21
|
-
"""The Update Diagnosis command's field values."""
|
|
22
|
-
return {
|
|
23
|
-
"condition_code": self.condition_code,
|
|
24
|
-
"new_condition_code": self.new_condition_code,
|
|
25
|
-
"background": self.background,
|
|
26
|
-
"narrative": self.narrative,
|
|
27
|
-
}
|
|
@@ -36,16 +36,3 @@ class UpdateGoalCommand(_BaseCommand):
|
|
|
36
36
|
achievement_status: AchievementStatus | None = None
|
|
37
37
|
priority: Priority | None = None
|
|
38
38
|
progress: str | None = None
|
|
39
|
-
|
|
40
|
-
@property
|
|
41
|
-
def values(self) -> dict:
|
|
42
|
-
"""The UpdateGoal command's field values."""
|
|
43
|
-
return {
|
|
44
|
-
"goal_id": self.goal_id,
|
|
45
|
-
"due_date": (self.due_date.isoformat() if self.due_date else None),
|
|
46
|
-
"achievement_status": (
|
|
47
|
-
self.achievement_status.value if self.achievement_status else None
|
|
48
|
-
),
|
|
49
|
-
"priority": (self.priority.value if self.priority else None),
|
|
50
|
-
"progress": self.progress,
|
|
51
|
-
}
|
|
@@ -52,29 +52,3 @@ class VitalsCommand(BaseCommand):
|
|
|
52
52
|
respiration_rate: conint(ge=6, le=60) | None = None # type: ignore[valid-type]
|
|
53
53
|
oxygen_saturation: conint(ge=60, le=100) | None = None # type: ignore[valid-type]
|
|
54
54
|
note: constr(max_length=150) | None = None # type: ignore[valid-type]
|
|
55
|
-
|
|
56
|
-
@property
|
|
57
|
-
def values(self) -> dict:
|
|
58
|
-
"""The Vitals command's field values."""
|
|
59
|
-
return {
|
|
60
|
-
"height": self.height,
|
|
61
|
-
"weight_lbs": self.weight_lbs,
|
|
62
|
-
"weight_oz": self.weight_oz,
|
|
63
|
-
"waist_circumference": self.waist_circumference,
|
|
64
|
-
"body_temperature": self.body_temperature,
|
|
65
|
-
"body_temperature_site": (
|
|
66
|
-
self.body_temperature_site.value if self.body_temperature_site else None
|
|
67
|
-
),
|
|
68
|
-
"blood_pressure_systole": self.blood_pressure_systole,
|
|
69
|
-
"blood_pressure_diastole": self.blood_pressure_diastole,
|
|
70
|
-
"blood_pressure_position_and_site": (
|
|
71
|
-
self.blood_pressure_position_and_site.value
|
|
72
|
-
if self.blood_pressure_position_and_site
|
|
73
|
-
else None
|
|
74
|
-
),
|
|
75
|
-
"pulse": self.pulse,
|
|
76
|
-
"pulse_rhythm": self.pulse_rhythm.value if self.pulse_rhythm else None,
|
|
77
|
-
"respiration_rate": self.respiration_rate,
|
|
78
|
-
"oxygen_saturation": self.oxygen_saturation,
|
|
79
|
-
"note": self.note,
|
|
80
|
-
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import uuid
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from canvas_sdk.commands.base import _BaseCommand
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DummyEnum(Enum):
|
|
11
|
+
"""A dummy enum class for testing purposes."""
|
|
12
|
+
|
|
13
|
+
LOW = "low"
|
|
14
|
+
HIGH = "high"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DummyCommand(_BaseCommand):
|
|
18
|
+
"""A dummy command class for testing purposes."""
|
|
19
|
+
|
|
20
|
+
class Meta:
|
|
21
|
+
key = "dummyCommand"
|
|
22
|
+
|
|
23
|
+
# Fields
|
|
24
|
+
int_field: int = 0
|
|
25
|
+
str_field: str = ""
|
|
26
|
+
enum_field: DummyEnum | None = None
|
|
27
|
+
date_field: datetime.date | None = None
|
|
28
|
+
uuid_field: uuid.UUID | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture
|
|
32
|
+
def dummy_command_instance() -> DummyCommand:
|
|
33
|
+
"""Fixture to return a mock instance of DummyCommand for testing."""
|
|
34
|
+
cmd = DummyCommand(int_field=10, str_field="hello")
|
|
35
|
+
# Set additional fields after instantiation.
|
|
36
|
+
cmd.enum_field = DummyEnum.HIGH
|
|
37
|
+
cmd.date_field = datetime.date(2025, 2, 14)
|
|
38
|
+
cmd.uuid_field = uuid.UUID("12345678-1234-5678-1234-567812345678")
|
|
39
|
+
# Set note_uuid and command_uuid for effect methods.
|
|
40
|
+
cmd.note_uuid = "note-123"
|
|
41
|
+
cmd.command_uuid = "cmd-456"
|
|
42
|
+
return cmd
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_dirty_keys(dummy_command_instance: DummyCommand) -> None:
|
|
46
|
+
"""Test that the dirty_keys property correctly tracks all fields that are set (via constructor and subsequent assignment)."""
|
|
47
|
+
keys = set(dummy_command_instance._dirty_keys)
|
|
48
|
+
expected_keys = {"int_field", "str_field", "enum_field", "date_field", "uuid_field"}
|
|
49
|
+
assert expected_keys == keys
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_values_transformation(dummy_command_instance: DummyCommand) -> None:
|
|
53
|
+
"""
|
|
54
|
+
Test that the values property applies type-specific transformations:
|
|
55
|
+
- Enums are replaced by their .value.
|
|
56
|
+
- Date/datetime fields are converted to ISO formatted strings.
|
|
57
|
+
- UUID fields are converted to strings.
|
|
58
|
+
- Other types are returned as-is.
|
|
59
|
+
"""
|
|
60
|
+
vals = dummy_command_instance.values
|
|
61
|
+
assert vals["int_field"] == 10
|
|
62
|
+
assert vals["str_field"] == "hello"
|
|
63
|
+
# For enum_field, should return its .value.
|
|
64
|
+
assert vals["enum_field"] == DummyEnum.HIGH.value
|
|
65
|
+
# For date_field, should return an ISO string.
|
|
66
|
+
|
|
67
|
+
assert (
|
|
68
|
+
vals["date_field"] == dummy_command_instance.date_field.isoformat()
|
|
69
|
+
if dummy_command_instance.date_field
|
|
70
|
+
else None
|
|
71
|
+
)
|
|
72
|
+
# For uuid_field, should return a string.
|
|
73
|
+
assert vals["uuid_field"] == str(dummy_command_instance.uuid_field)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_constantized_key(dummy_command_instance: DummyCommand) -> None:
|
|
77
|
+
"""
|
|
78
|
+
Test that constantized_key transforms the Meta.key from 'dummyCommand'
|
|
79
|
+
into an uppercase, underscore-separated string ('DUMMY_COMMAND').
|
|
80
|
+
"""
|
|
81
|
+
assert dummy_command_instance.constantized_key() == "DUMMY_COMMAND"
|