canvas 0.19.1__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.1.dist-info → canvas-0.20.0.dist-info}/METADATA +1 -1
- {canvas-0.19.1.dist-info → canvas-0.20.0.dist-info}/RECORD +46 -44
- 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/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} +23 -11
- 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
- {canvas-0.19.1.dist-info → canvas-0.20.0.dist-info}/WHEEL +0 -0
- {canvas-0.19.1.dist-info → canvas-0.20.0.dist-info}/entry_points.txt +0 -0
|
@@ -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"
|
canvas_sdk/utils/stats.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
from datetime import timedelta
|
|
1
2
|
from time import time
|
|
2
3
|
from typing import Any
|
|
3
4
|
|
|
5
|
+
from statsd.defaults.env import statsd as default_statsd_client
|
|
6
|
+
|
|
4
7
|
|
|
5
8
|
def get_duration_ms(start_time: float) -> int:
|
|
6
9
|
"""Get the duration in milliseconds since the given start time."""
|
|
@@ -26,3 +29,35 @@ def tags_to_line_protocol(tags: dict[str, Any]) -> str:
|
|
|
26
29
|
f"{tag_name}={str(tag_value).translate(LINE_PROTOCOL_TRANSLATION)}"
|
|
27
30
|
for tag_name, tag_value in tags.items()
|
|
28
31
|
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class StatsDClientProxy:
|
|
35
|
+
"""Proxy for a StatsD client."""
|
|
36
|
+
|
|
37
|
+
def __init__(self) -> None:
|
|
38
|
+
self.client = default_statsd_client
|
|
39
|
+
|
|
40
|
+
def gauge(self, metric_name: str, value: float, tags: dict[str, str]) -> None:
|
|
41
|
+
"""Sends a gauge metric to StatsD with properly formatted tags.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
metric_name (str): The name of the metric.
|
|
45
|
+
value (float): The value to report.
|
|
46
|
+
tags (dict[str, str]): Dictionary of tags to attach to the metric.
|
|
47
|
+
"""
|
|
48
|
+
statsd_tags = tags_to_line_protocol(tags)
|
|
49
|
+
self.client.gauge(f"{metric_name},{statsd_tags}", value)
|
|
50
|
+
|
|
51
|
+
def timing(self, metric_name: str, delta: float | timedelta, tags: dict[str, str]) -> None:
|
|
52
|
+
"""Sends a timing metric to StatsD with properly formatted tags.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
metric_name (str): The name of the metric.
|
|
56
|
+
delta (float | timedelta): The value to report.
|
|
57
|
+
tags (dict[str, str]): Dictionary of tags to attach to the metric.
|
|
58
|
+
"""
|
|
59
|
+
statsd_tags = tags_to_line_protocol(tags)
|
|
60
|
+
self.client.timing(f"{metric_name},{statsd_tags}", delta)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
statsd_client = StatsDClientProxy()
|