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.

Files changed (46) hide show
  1. {canvas-0.19.1.dist-info → canvas-0.20.0.dist-info}/METADATA +1 -1
  2. {canvas-0.19.1.dist-info → canvas-0.20.0.dist-info}/RECORD +46 -44
  3. canvas_generated/messages/effects_pb2.py +2 -2
  4. canvas_generated/messages/effects_pb2.pyi +10 -0
  5. canvas_generated/messages/events_pb2.py +2 -2
  6. canvas_generated/messages/events_pb2.pyi +32 -0
  7. canvas_sdk/commands/__init__.py +2 -0
  8. canvas_sdk/commands/base.py +47 -3
  9. canvas_sdk/commands/commands/allergy.py +0 -12
  10. canvas_sdk/commands/commands/assess.py +0 -10
  11. canvas_sdk/commands/commands/close_goal.py +0 -11
  12. canvas_sdk/commands/commands/diagnose.py +0 -14
  13. canvas_sdk/commands/commands/family_history.py +0 -5
  14. canvas_sdk/commands/commands/follow_up.py +69 -0
  15. canvas_sdk/commands/commands/goal.py +0 -14
  16. canvas_sdk/commands/commands/history_present_illness.py +0 -5
  17. canvas_sdk/commands/commands/instruct.py +0 -5
  18. canvas_sdk/commands/commands/lab_order.py +59 -11
  19. canvas_sdk/commands/commands/medical_history.py +0 -15
  20. canvas_sdk/commands/commands/medication_statement.py +0 -5
  21. canvas_sdk/commands/commands/past_surgical_history.py +0 -11
  22. canvas_sdk/commands/commands/perform.py +0 -5
  23. canvas_sdk/commands/commands/plan.py +0 -5
  24. canvas_sdk/commands/commands/prescribe.py +7 -14
  25. canvas_sdk/commands/commands/questionnaire.py +0 -5
  26. canvas_sdk/commands/commands/reason_for_visit.py +0 -9
  27. canvas_sdk/commands/commands/remove_allergy.py +0 -8
  28. canvas_sdk/commands/commands/stop_medication.py +0 -5
  29. canvas_sdk/commands/commands/task.py +0 -12
  30. canvas_sdk/commands/commands/update_diagnosis.py +0 -10
  31. canvas_sdk/commands/commands/update_goal.py +0 -13
  32. canvas_sdk/commands/commands/vitals.py +0 -26
  33. canvas_sdk/commands/tests/test_base_command.py +81 -0
  34. canvas_sdk/effects/launch_modal.py +1 -0
  35. canvas_sdk/utils/stats.py +35 -0
  36. canvas_sdk/v1/data/lab.py +38 -0
  37. canvas_sdk/v1/data/note.py +3 -0
  38. logger/logger.py +8 -1
  39. plugin_runner/{plugin_installer.py → installation.py} +23 -11
  40. plugin_runner/plugin_runner.py +70 -40
  41. plugin_runner/tests/test_plugin_installer.py +3 -3
  42. plugin_runner/tests/test_plugin_runner.py +1 -1
  43. protobufs/canvas_generated/messages/effects.proto +6 -0
  44. protobufs/canvas_generated/messages/events.proto +16 -12
  45. {canvas-0.19.1.dist-info → canvas-0.20.0.dist-info}/WHEEL +0 -0
  46. {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
- }
@@ -9,8 +9,3 @@ class HistoryOfPresentIllnessCommand(_BaseCommand):
9
9
  commit_required_fields = ("narrative",)
10
10
 
11
11
  narrative: str | None = None
12
-
13
- @property
14
- def values(self) -> dict:
15
- """The HPI command's field values."""
16
- return {"narrative": self.narrative}
@@ -10,8 +10,3 @@ class InstructCommand(BaseCommand):
10
10
 
11
11
  instruction: str | None = None
12
12
  comment: str | None = None
13
-
14
- @property
15
- def values(self) -> dict:
16
- """The Instruct command's field values."""
17
- return {"instruction": self.instruction, "comment": self.comment}
@@ -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
- @property
24
- def values(self) -> dict:
25
- """The Lab Order command's field values."""
26
- return {
27
- "lab_partner": self.lab_partner,
28
- "tests_order_codes": self.tests_order_codes,
29
- "ordering_provider_key": self.ordering_provider_key,
30
- "diagnosis_codes": self.diagnosis_codes,
31
- "fasting_required": self.fasting_required,
32
- "comment": self.comment,
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
- }
@@ -10,8 +10,3 @@ class PerformCommand(BaseCommand):
10
10
 
11
11
  cpt_code: str
12
12
  notes: str | None = None
13
-
14
- @property
15
- def values(self) -> dict:
16
- """The Perform command's field values."""
17
- return {"cpt_code": self.cpt_code, "notes": self.notes}
@@ -9,8 +9,3 @@ class PlanCommand(_BaseCommand):
9
9
  commit_required_fields = ("narrative",)
10
10
 
11
11
  narrative: str = ""
12
-
13
- @property
14
- def values(self) -> dict:
15
- """The Plan command's field values."""
16
- return {"narrative": self.narrative}
@@ -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
- return {
49
- "fdb_code": self.fdb_code,
50
- "icd10_codes": self.icd10_codes,
51
- "sig": self.sig,
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
- "type_to_dispense": self.type_to_dispense if self.type_to_dispense else None,
57
- "refills": self.refills,
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"
@@ -16,6 +16,7 @@ class LaunchModalEffect(_BaseEffect):
16
16
  DEFAULT_MODAL = "default_modal"
17
17
  NEW_WINDOW = "new_window"
18
18
  RIGHT_CHART_PANE = "right_chart_pane"
19
+ RIGHT_CHART_PANE_LARGE = "right_chart_pane_large"
19
20
 
20
21
  url: str | None = None
21
22
  content: str | None = None
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()