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.
Files changed (185) hide show
  1. {canvas-0.63.0.dist-info → canvas-0.89.0.dist-info}/METADATA +4 -1
  2. {canvas-0.63.0.dist-info → canvas-0.89.0.dist-info}/RECORD +184 -98
  3. {canvas-0.63.0.dist-info → canvas-0.89.0.dist-info}/WHEEL +1 -1
  4. canvas_cli/apps/emit/event_fixtures/UNKNOWN.ndjson +1 -0
  5. canvas_cli/apps/logs/logs.py +386 -22
  6. canvas_cli/main.py +3 -1
  7. canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/tests/test_models.py +46 -4
  8. canvas_cli/utils/context/context.py +13 -13
  9. canvas_cli/utils/validators/manifest_schema.py +26 -1
  10. canvas_generated/messages/effects_pb2.py +5 -5
  11. canvas_generated/messages/effects_pb2.pyi +108 -2
  12. canvas_generated/messages/events_pb2.py +6 -6
  13. canvas_generated/messages/events_pb2.pyi +282 -2
  14. canvas_sdk/clients/__init__.py +1 -0
  15. canvas_sdk/clients/llms/__init__.py +17 -0
  16. canvas_sdk/clients/llms/libraries/__init__.py +11 -0
  17. canvas_sdk/clients/llms/libraries/llm_anthropic.py +87 -0
  18. canvas_sdk/clients/llms/libraries/llm_api.py +143 -0
  19. canvas_sdk/clients/llms/libraries/llm_google.py +92 -0
  20. canvas_sdk/clients/llms/libraries/llm_openai.py +98 -0
  21. canvas_sdk/clients/llms/structures/__init__.py +9 -0
  22. canvas_sdk/clients/llms/structures/llm_response.py +33 -0
  23. canvas_sdk/clients/llms/structures/llm_tokens.py +53 -0
  24. canvas_sdk/clients/llms/structures/llm_turn.py +47 -0
  25. canvas_sdk/clients/llms/structures/settings/__init__.py +13 -0
  26. canvas_sdk/clients/llms/structures/settings/llm_settings.py +27 -0
  27. canvas_sdk/clients/llms/structures/settings/llm_settings_anthropic.py +43 -0
  28. canvas_sdk/clients/llms/structures/settings/llm_settings_gemini.py +40 -0
  29. canvas_sdk/clients/llms/structures/settings/llm_settings_gpt4.py +40 -0
  30. canvas_sdk/clients/llms/structures/settings/llm_settings_gpt5.py +48 -0
  31. canvas_sdk/clients/third_party.py +3 -0
  32. canvas_sdk/commands/__init__.py +12 -0
  33. canvas_sdk/commands/base.py +33 -2
  34. canvas_sdk/commands/commands/adjust_prescription.py +4 -0
  35. canvas_sdk/commands/commands/custom_command.py +86 -0
  36. canvas_sdk/commands/commands/family_history.py +17 -1
  37. canvas_sdk/commands/commands/immunization_statement.py +42 -2
  38. canvas_sdk/commands/commands/medication_statement.py +16 -1
  39. canvas_sdk/commands/commands/past_surgical_history.py +16 -1
  40. canvas_sdk/commands/commands/perform.py +18 -1
  41. canvas_sdk/commands/commands/prescribe.py +8 -9
  42. canvas_sdk/commands/commands/refill.py +5 -5
  43. canvas_sdk/commands/commands/resolve_condition.py +5 -5
  44. canvas_sdk/commands/commands/review/__init__.py +3 -0
  45. canvas_sdk/commands/commands/review/base.py +72 -0
  46. canvas_sdk/commands/commands/review/imaging.py +13 -0
  47. canvas_sdk/commands/commands/review/lab.py +13 -0
  48. canvas_sdk/commands/commands/review/referral.py +13 -0
  49. canvas_sdk/commands/commands/review/uncategorized_document.py +13 -0
  50. canvas_sdk/commands/validation.py +43 -0
  51. canvas_sdk/effects/batch_originate.py +22 -0
  52. canvas_sdk/effects/calendar/__init__.py +13 -3
  53. canvas_sdk/effects/calendar/{create_calendar.py → calendar.py} +19 -5
  54. canvas_sdk/effects/calendar/event.py +172 -0
  55. canvas_sdk/effects/claim_label.py +93 -0
  56. canvas_sdk/effects/claim_line_item.py +47 -0
  57. canvas_sdk/effects/claim_queue.py +49 -0
  58. canvas_sdk/effects/fax/__init__.py +3 -0
  59. canvas_sdk/effects/fax/base.py +77 -0
  60. canvas_sdk/effects/fax/note.py +42 -0
  61. canvas_sdk/effects/metadata.py +15 -1
  62. canvas_sdk/effects/note/__init__.py +8 -1
  63. canvas_sdk/effects/note/appointment.py +135 -7
  64. canvas_sdk/effects/note/base.py +17 -0
  65. canvas_sdk/effects/note/message.py +22 -14
  66. canvas_sdk/effects/note/note.py +150 -1
  67. canvas_sdk/effects/observation/__init__.py +11 -0
  68. canvas_sdk/effects/observation/base.py +206 -0
  69. canvas_sdk/effects/patient/__init__.py +2 -0
  70. canvas_sdk/effects/patient/base.py +8 -0
  71. canvas_sdk/effects/payment/__init__.py +11 -0
  72. canvas_sdk/effects/payment/base.py +355 -0
  73. canvas_sdk/effects/payment/post_claim_payment.py +49 -0
  74. canvas_sdk/effects/send_contact_verification.py +42 -0
  75. canvas_sdk/effects/task/__init__.py +2 -1
  76. canvas_sdk/effects/task/task.py +30 -0
  77. canvas_sdk/effects/validation/__init__.py +3 -0
  78. canvas_sdk/effects/validation/base.py +92 -0
  79. canvas_sdk/events/base.py +15 -0
  80. canvas_sdk/handlers/application.py +7 -7
  81. canvas_sdk/handlers/simple_api/api.py +1 -4
  82. canvas_sdk/handlers/simple_api/websocket.py +1 -4
  83. canvas_sdk/handlers/utils.py +14 -0
  84. canvas_sdk/questionnaires/utils.py +1 -0
  85. canvas_sdk/templates/utils.py +17 -4
  86. canvas_sdk/test_utils/factories/FACTORY_GUIDE.md +362 -0
  87. canvas_sdk/test_utils/factories/__init__.py +115 -0
  88. canvas_sdk/test_utils/factories/calendar.py +24 -0
  89. canvas_sdk/test_utils/factories/claim.py +81 -0
  90. canvas_sdk/test_utils/factories/claim_diagnosis_code.py +16 -0
  91. canvas_sdk/test_utils/factories/coverage.py +17 -0
  92. canvas_sdk/test_utils/factories/imaging.py +74 -0
  93. canvas_sdk/test_utils/factories/lab.py +192 -0
  94. canvas_sdk/test_utils/factories/medication_history.py +75 -0
  95. canvas_sdk/test_utils/factories/note.py +52 -0
  96. canvas_sdk/test_utils/factories/organization.py +50 -0
  97. canvas_sdk/test_utils/factories/practicelocation.py +88 -0
  98. canvas_sdk/test_utils/factories/referral.py +81 -0
  99. canvas_sdk/test_utils/factories/staff.py +111 -0
  100. canvas_sdk/test_utils/factories/task.py +66 -0
  101. canvas_sdk/test_utils/factories/uncategorized_clinical_document.py +48 -0
  102. canvas_sdk/utils/metrics.py +4 -1
  103. canvas_sdk/v1/data/__init__.py +66 -7
  104. canvas_sdk/v1/data/allergy_intolerance.py +5 -11
  105. canvas_sdk/v1/data/appointment.py +18 -4
  106. canvas_sdk/v1/data/assessment.py +2 -12
  107. canvas_sdk/v1/data/banner_alert.py +2 -4
  108. canvas_sdk/v1/data/base.py +53 -14
  109. canvas_sdk/v1/data/billing.py +8 -11
  110. canvas_sdk/v1/data/calendar.py +64 -0
  111. canvas_sdk/v1/data/care_team.py +4 -10
  112. canvas_sdk/v1/data/claim.py +172 -66
  113. canvas_sdk/v1/data/claim_diagnosis_code.py +19 -0
  114. canvas_sdk/v1/data/claim_line_item.py +2 -5
  115. canvas_sdk/v1/data/coding.py +19 -0
  116. canvas_sdk/v1/data/command.py +2 -4
  117. canvas_sdk/v1/data/common.py +10 -0
  118. canvas_sdk/v1/data/compound_medication.py +3 -4
  119. canvas_sdk/v1/data/condition.py +4 -9
  120. canvas_sdk/v1/data/coverage.py +66 -26
  121. canvas_sdk/v1/data/detected_issue.py +20 -20
  122. canvas_sdk/v1/data/device.py +2 -14
  123. canvas_sdk/v1/data/discount.py +2 -5
  124. canvas_sdk/v1/data/encounter.py +44 -0
  125. canvas_sdk/v1/data/facility.py +1 -0
  126. canvas_sdk/v1/data/goal.py +2 -14
  127. canvas_sdk/v1/data/imaging.py +4 -30
  128. canvas_sdk/v1/data/immunization.py +7 -15
  129. canvas_sdk/v1/data/lab.py +12 -65
  130. canvas_sdk/v1/data/line_item_transaction.py +2 -5
  131. canvas_sdk/v1/data/medication.py +3 -8
  132. canvas_sdk/v1/data/medication_history.py +142 -0
  133. canvas_sdk/v1/data/medication_statement.py +41 -0
  134. canvas_sdk/v1/data/message.py +4 -8
  135. canvas_sdk/v1/data/note.py +37 -38
  136. canvas_sdk/v1/data/observation.py +9 -36
  137. canvas_sdk/v1/data/organization.py +70 -9
  138. canvas_sdk/v1/data/patient.py +8 -12
  139. canvas_sdk/v1/data/patient_consent.py +4 -14
  140. canvas_sdk/v1/data/payment_collection.py +2 -5
  141. canvas_sdk/v1/data/posting.py +3 -9
  142. canvas_sdk/v1/data/practicelocation.py +66 -7
  143. canvas_sdk/v1/data/protocol_override.py +3 -4
  144. canvas_sdk/v1/data/protocol_result.py +3 -3
  145. canvas_sdk/v1/data/questionnaire.py +10 -26
  146. canvas_sdk/v1/data/reason_for_visit.py +2 -6
  147. canvas_sdk/v1/data/referral.py +41 -17
  148. canvas_sdk/v1/data/staff.py +34 -26
  149. canvas_sdk/v1/data/stop_medication_event.py +27 -0
  150. canvas_sdk/v1/data/task.py +30 -11
  151. canvas_sdk/v1/data/team.py +2 -4
  152. canvas_sdk/v1/data/uncategorized_clinical_document.py +84 -0
  153. canvas_sdk/v1/data/user.py +14 -0
  154. canvas_sdk/v1/data/utils.py +5 -0
  155. canvas_sdk/value_set/v2026/__init__.py +1 -0
  156. canvas_sdk/value_set/v2026/adverse_event.py +157 -0
  157. canvas_sdk/value_set/v2026/allergy.py +116 -0
  158. canvas_sdk/value_set/v2026/assessment.py +466 -0
  159. canvas_sdk/value_set/v2026/communication.py +496 -0
  160. canvas_sdk/value_set/v2026/condition.py +52934 -0
  161. canvas_sdk/value_set/v2026/device.py +315 -0
  162. canvas_sdk/value_set/v2026/diagnostic_study.py +5243 -0
  163. canvas_sdk/value_set/v2026/encounter.py +2714 -0
  164. canvas_sdk/value_set/v2026/immunization.py +297 -0
  165. canvas_sdk/value_set/v2026/individual_characteristic.py +339 -0
  166. canvas_sdk/value_set/v2026/intervention.py +1703 -0
  167. canvas_sdk/value_set/v2026/laboratory_test.py +1831 -0
  168. canvas_sdk/value_set/v2026/medication.py +8218 -0
  169. canvas_sdk/value_set/v2026/no_qdm_category_assigned.py +26493 -0
  170. canvas_sdk/value_set/v2026/physical_exam.py +342 -0
  171. canvas_sdk/value_set/v2026/procedure.py +27869 -0
  172. canvas_sdk/value_set/v2026/symptom.py +625 -0
  173. logger/logger.py +30 -31
  174. logger/logstash.py +282 -0
  175. logger/pubsub.py +26 -0
  176. plugin_runner/allowed-module-imports.json +940 -9
  177. plugin_runner/generate_allowed_imports.py +1 -0
  178. plugin_runner/installation.py +2 -2
  179. plugin_runner/plugin_runner.py +21 -24
  180. plugin_runner/sandbox.py +34 -0
  181. protobufs/canvas_generated/messages/effects.proto +65 -0
  182. protobufs/canvas_generated/messages/events.proto +150 -51
  183. settings.py +27 -11
  184. canvas_sdk/effects/calendar/create_event.py +0 -43
  185. {canvas-0.63.0.dist-info → canvas-0.89.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,40 @@
1
+ from dataclasses import dataclass
2
+
3
+ from canvas_sdk.clients.llms.structures.settings.llm_settings import LlmSettings
4
+
5
+
6
+ @dataclass
7
+ class LlmSettingsGemini(LlmSettings):
8
+ """Configuration settings for Google Gemini LLM API.
9
+
10
+ Extends LlmSettings with Gemini-specific parameters.
11
+
12
+ Attributes:
13
+ api_key: API authentication key for the LLM service (inherited).
14
+ model: Name or identifier of the LLM model to use (inherited).
15
+ temperature: Controls randomness in responses (0.0-1.0).
16
+
17
+ Example:
18
+ ```python3
19
+ LlmSettingsGemini(
20
+ api_key=environ.get("google_key"),
21
+ model="models/gemini-2.0-flash",
22
+ temperature=2.0,
23
+ )
24
+ ```
25
+ """
26
+
27
+ temperature: float
28
+
29
+ def to_dict(self) -> dict:
30
+ """Convert settings to Google Gemini API request format.
31
+
32
+ Returns:
33
+ Dictionary containing model name and generationConfig with temperature.
34
+ """
35
+ return super().to_dict() | {
36
+ "generationConfig": {"temperature": self.temperature},
37
+ }
38
+
39
+
40
+ __exports__ = ("LlmSettingsGemini",)
@@ -0,0 +1,40 @@
1
+ from dataclasses import dataclass
2
+
3
+ from canvas_sdk.clients.llms.structures.settings.llm_settings import LlmSettings
4
+
5
+
6
+ @dataclass
7
+ class LlmSettingsGpt4(LlmSettings):
8
+ """Configuration settings for OpenAI GPT-4 LLM API.
9
+
10
+ Extends LlmSettings with GPT-4-specific parameters.
11
+
12
+ Attributes:
13
+ api_key: API authentication key for the LLM service (inherited).
14
+ model: Name or identifier of the LLM model to use (inherited).
15
+ temperature: Controls randomness in responses (0.0-1.0).
16
+
17
+ Example:
18
+ ```python3
19
+ LlmSettingsGpt4(
20
+ api_key=environ.get("openai_key"),
21
+ model="gpt-4o",
22
+ temperature=2.0,
23
+ )
24
+ ```
25
+ """
26
+
27
+ temperature: float
28
+
29
+ def to_dict(self) -> dict:
30
+ """Convert settings to OpenAI GPT-4 API request format.
31
+
32
+ Returns:
33
+ Dictionary containing model name and temperature.
34
+ """
35
+ return super().to_dict() | {
36
+ "temperature": self.temperature,
37
+ }
38
+
39
+
40
+ __exports__ = ("LlmSettingsGpt4",)
@@ -0,0 +1,48 @@
1
+ from dataclasses import dataclass
2
+
3
+ from canvas_sdk.clients.llms.structures.settings.llm_settings import LlmSettings
4
+
5
+
6
+ @dataclass
7
+ class LlmSettingsGpt5(LlmSettings):
8
+ """Configuration settings for OpenAI GPT-5 LLM API.
9
+
10
+ Extends LlmSettings with GPT-5-specific parameters for reasoning and text generation.
11
+
12
+ Attributes:
13
+ api_key: API authentication key for the LLM service (inherited).
14
+ model: Name or identifier of the LLM model to use (inherited).
15
+ reasoning_effort: Level of reasoning effort ('none', 'low', 'medium', 'high').
16
+ text_verbosity: Level of text verbosity in responses ('low', 'medium', 'high').
17
+
18
+ Example:
19
+ ```python3
20
+ LlmSettingsGpt5(
21
+ api_key=environ.get("openai_key"),
22
+ model="gpt-5.1",
23
+ reasoning_effort="none",
24
+ text_verbosity="low",
25
+ )
26
+ ```
27
+ """
28
+
29
+ reasoning_effort: str
30
+ text_verbosity: str
31
+
32
+ def to_dict(self) -> dict:
33
+ """Convert settings to OpenAI GPT-5 API request format.
34
+
35
+ Returns:
36
+ Dictionary containing model name, reasoning config, and text config.
37
+ """
38
+ return super().to_dict() | {
39
+ "reasoning": {
40
+ "effort": self.reasoning_effort,
41
+ },
42
+ "text": {
43
+ "verbosity": self.text_verbosity,
44
+ },
45
+ }
46
+
47
+
48
+ __exports__ = ("LlmSettingsGpt5",)
@@ -0,0 +1,3 @@
1
+ from stripe import StripeClient
2
+
3
+ __all__ = __exports__ = ("StripeClient",)
@@ -3,6 +3,7 @@ from canvas_sdk.commands.commands.allergy import AllergyCommand
3
3
  from canvas_sdk.commands.commands.assess import AssessCommand
4
4
  from canvas_sdk.commands.commands.chart_section_review import ChartSectionReviewCommand
5
5
  from canvas_sdk.commands.commands.close_goal import CloseGoalCommand
6
+ from canvas_sdk.commands.commands.custom_command import CustomCommand
6
7
  from canvas_sdk.commands.commands.diagnose import DiagnoseCommand
7
8
  from canvas_sdk.commands.commands.exam import PhysicalExamCommand
8
9
  from canvas_sdk.commands.commands.family_history import FamilyHistoryCommand
@@ -28,6 +29,12 @@ from canvas_sdk.commands.commands.refer import ReferCommand
28
29
  from canvas_sdk.commands.commands.refill import RefillCommand
29
30
  from canvas_sdk.commands.commands.remove_allergy import RemoveAllergyCommand
30
31
  from canvas_sdk.commands.commands.resolve_condition import ResolveConditionCommand
32
+ from canvas_sdk.commands.commands.review.imaging import ImagingReviewCommand
33
+ from canvas_sdk.commands.commands.review.lab import LabReviewCommand
34
+ from canvas_sdk.commands.commands.review.referral import ReferralReviewCommand
35
+ from canvas_sdk.commands.commands.review.uncategorized_document import (
36
+ UncategorizedDocumentReviewCommand,
37
+ )
31
38
  from canvas_sdk.commands.commands.review_of_systems import ReviewOfSystemsCommand
32
39
  from canvas_sdk.commands.commands.stop_medication import StopMedicationCommand
33
40
  from canvas_sdk.commands.commands.structured_assessment import StructuredAssessmentCommand
@@ -47,9 +54,12 @@ __all__ = __exports__ = (
47
54
  "FollowUpCommand",
48
55
  "GoalCommand",
49
56
  "HistoryOfPresentIllnessCommand",
57
+ "CustomCommand",
50
58
  "ImagingOrderCommand",
59
+ "ImagingReviewCommand",
51
60
  "InstructCommand",
52
61
  "LabOrderCommand",
62
+ "LabReviewCommand",
53
63
  "MedicalHistoryCommand",
54
64
  "MedicationStatementCommand",
55
65
  "PastSurgicalHistoryCommand",
@@ -60,6 +70,7 @@ __all__ = __exports__ = (
60
70
  "QuestionnaireCommand",
61
71
  "ReasonForVisitCommand",
62
72
  "ReferCommand",
73
+ "ReferralReviewCommand",
63
74
  "RefillCommand",
64
75
  "RemoveAllergyCommand",
65
76
  "ResolveConditionCommand",
@@ -67,6 +78,7 @@ __all__ = __exports__ = (
67
78
  "StopMedicationCommand",
68
79
  "StructuredAssessmentCommand",
69
80
  "TaskCommand",
81
+ "UncategorizedDocumentReviewCommand",
70
82
  "UpdateDiagnosisCommand",
71
83
  "UpdateGoalCommand",
72
84
  "VitalsCommand",
@@ -18,6 +18,7 @@ class _BaseCommand(TrackableFieldsModel):
18
18
  originate_required_fields = ("note_uuid",)
19
19
  edit_required_fields = ("command_uuid",)
20
20
  send_required_fields = ("command_uuid",)
21
+ review_required_fields = ("command_uuid",)
21
22
  delete_required_fields = ("command_uuid",)
22
23
  commit_required_fields = ("command_uuid",)
23
24
  enter_in_error_required_fields = ("command_uuid",)
@@ -29,11 +30,16 @@ class _BaseCommand(TrackableFieldsModel):
29
30
 
30
31
  def __init__(self, /, **data: Any) -> None:
31
32
  """Initialize the command and mark all provided keys as dirty."""
33
+ if getattr(self.Meta, "abstract", False):
34
+ raise TypeError(f"Cannot instantiate abstract class {self.__class__.__name__!r}.")
35
+
32
36
  super().__init__(**data)
33
37
 
34
38
  def __init_subclass__(cls, **kwargs: Any) -> None:
35
39
  """Validate that the command has a key and required fields."""
36
- if not hasattr(cls.Meta, "key") or not cls.Meta.key:
40
+ if (not hasattr(cls.Meta, "key") or not cls.Meta.key) and not getattr(
41
+ cls.Meta, "abstract", False
42
+ ):
37
43
  raise ImproperlyConfigured(f"Command {cls.__name__!r} must specify Meta.key.")
38
44
 
39
45
  if hasattr(cls.Meta, "commit_required_fields"):
@@ -118,6 +124,17 @@ class _BaseCommand(TrackableFieldsModel):
118
124
  ),
119
125
  )
120
126
 
127
+ def _origination_payload_for_batch(self, line_number: int = -1) -> dict:
128
+ """Originate a new command in the note body for batch processing."""
129
+ self._validate_before_effect("originate")
130
+ return {
131
+ "type": f"ORIGINATE_{self.constantized_key()}_COMMAND",
132
+ "command": self.command_uuid,
133
+ "note": self.note_uuid,
134
+ "data": self.values,
135
+ "line_number": line_number,
136
+ }
137
+
121
138
  def edit(self) -> Effect:
122
139
  """Edit the command."""
123
140
  self._validate_before_effect("edit")
@@ -178,4 +195,18 @@ class _SendableCommandMixin:
178
195
  )
179
196
 
180
197
 
181
- __exports__ = ("_BaseCommand", "_SendableCommandMixin")
198
+ class _ReviewableCommandMixin:
199
+ def review(self) -> Effect:
200
+ """Fire the review effect the command."""
201
+ self._validate_before_effect("review") # type: ignore[attr-defined]
202
+ return Effect(
203
+ type=f"REVIEW_{self.constantized_key()}_COMMAND", # type: ignore[attr-defined]
204
+ payload=json.dumps({"command": self.command_uuid}), # type: ignore[attr-defined]
205
+ )
206
+
207
+
208
+ __exports__ = (
209
+ "_BaseCommand",
210
+ "_SendableCommandMixin",
211
+ "_ReviewableCommandMixin",
212
+ )
@@ -13,5 +13,9 @@ class AdjustPrescriptionCommand(RefillCommand):
13
13
  default=None, json_schema_extra={"commands_api_name": "change_medication_to"}
14
14
  )
15
15
 
16
+ def _has_fdb_code(self) -> bool:
17
+ """Check if new_fdb_code is provided and non-empty."""
18
+ return self.new_fdb_code is not None and self.new_fdb_code.strip() != ""
19
+
16
20
 
17
21
  __exports__ = ("AdjustPrescriptionCommand",)
@@ -0,0 +1,86 @@
1
+ from typing import Any
2
+
3
+ from pydantic_core import InitErrorDetails
4
+
5
+ from canvas_sdk.commands.base import _BaseCommand
6
+
7
+
8
+ class CustomCommand(_BaseCommand):
9
+ """A class for managing a custom command within a specific note.
10
+
11
+ This class can be extended to create custom commands with predefined schema_key
12
+ or passing schema_key directly to the constructor.
13
+ """
14
+
15
+ class Meta:
16
+ key = "customCommand"
17
+ schema_key: str | None = None
18
+
19
+ _schema_key: str | None = None
20
+ content: str | None = None
21
+ print_content: str | None = None
22
+
23
+ def __init_subclass__(cls, **kwargs: Any) -> None:
24
+ """Ensure Meta.key is always 'customCommand' for all CustomCommand subclasses."""
25
+ # Always enforce key = "customCommand" for all subclasses
26
+ cls.Meta.key = "customCommand"
27
+
28
+ # Call parent __init_subclass__ to perform validation
29
+ super().__init_subclass__(**kwargs)
30
+
31
+ def __init__(self, **data: Any) -> None:
32
+ """Initialize with schema_key from Meta if not provided."""
33
+ meta_schema_key = getattr(self.__class__.Meta, "schema_key", None)
34
+
35
+ # Extract schema_key before init if provided
36
+ instance_schema_key = data.pop("schema_key", None)
37
+
38
+ # If Meta defines schema_key, use it and don't allow override
39
+ if meta_schema_key is not None and instance_schema_key is not None:
40
+ raise AttributeError(
41
+ f"Cannot set schema_key on {self.__class__.__name__} instance. "
42
+ f"schema_key is already defined in Meta as '{meta_schema_key}'."
43
+ )
44
+
45
+ super().__init__(**data)
46
+
47
+ # Set _schema_key after init to ensure it's tracked
48
+ if meta_schema_key is None and instance_schema_key is not None:
49
+ self._schema_key = instance_schema_key
50
+
51
+ @property
52
+ def schema_key(self) -> str | None:
53
+ """Get schema_key from Meta if defined, otherwise from instance attribute."""
54
+ meta_schema_key = getattr(self.__class__.Meta, "schema_key", None)
55
+ return meta_schema_key if meta_schema_key is not None else self._schema_key
56
+
57
+ @property
58
+ def values(self) -> dict:
59
+ """Get values for the custom command."""
60
+ return {**super().values, "schema_key": self.schema_key}
61
+
62
+ def _get_error_details(self, method: str) -> list[InitErrorDetails]:
63
+ """Get error details for the custom command."""
64
+ errors = super()._get_error_details(method)
65
+
66
+ if method == "originate":
67
+ if not self.content:
68
+ errors.append(
69
+ self._create_error_detail(
70
+ "value",
71
+ "Content must be provided for a custom command.",
72
+ self.content,
73
+ )
74
+ )
75
+ if not self.schema_key:
76
+ errors.append(
77
+ self._create_error_detail(
78
+ "value",
79
+ "Schema key must be provided for a custom command.",
80
+ self.schema_key,
81
+ )
82
+ )
83
+ return errors
84
+
85
+
86
+ __exports__ = ("CustomCommand",)
@@ -1,4 +1,7 @@
1
+ from pydantic_core import InitErrorDetails
2
+
1
3
  from canvas_sdk.commands.base import _BaseCommand as BaseCommand
4
+ from canvas_sdk.commands.constants import CodeSystems, Coding
2
5
 
3
6
 
4
7
  class FamilyHistoryCommand(BaseCommand):
@@ -7,9 +10,22 @@ class FamilyHistoryCommand(BaseCommand):
7
10
  class Meta:
8
11
  key = "familyHistory"
9
12
 
10
- family_history: str | None = None
13
+ family_history: str | Coding | None = None
11
14
  relative: str | None = None
12
15
  note: str | None = None
13
16
 
17
+ def _get_error_details(self, method: str) -> list[InitErrorDetails]:
18
+ errors = super()._get_error_details(method)
19
+
20
+ if (
21
+ isinstance(self.family_history, dict)
22
+ and self.family_history["system"] != CodeSystems.SNOMED
23
+ and self.family_history["system"] != CodeSystems.UNSTRUCTURED
24
+ ):
25
+ message = f"The 'coding.system' field must be '{CodeSystems.SNOMED}' or '{CodeSystems.UNSTRUCTURED}'."
26
+ errors.append(self._create_error_detail("value", message, self.family_history))
27
+
28
+ return errors
29
+
14
30
 
15
31
  __exports__ = ("FamilyHistoryCommand",)
@@ -3,6 +3,7 @@ from datetime import date
3
3
  from pydantic_core import InitErrorDetails
4
4
 
5
5
  from canvas_sdk.commands.base import _BaseCommand as BaseCommand
6
+ from canvas_sdk.commands.constants import CodeSystems, Coding
6
7
 
7
8
 
8
9
  class ImmunizationStatementCommand(BaseCommand):
@@ -11,14 +12,53 @@ class ImmunizationStatementCommand(BaseCommand):
11
12
  class Meta:
12
13
  key = "immunizationStatement"
13
14
 
14
- cpt_code: str
15
- cvx_code: str
15
+ cpt_code: str | Coding | None = None
16
+ cvx_code: str | Coding | None = None
17
+ unstructured: Coding | None = None
16
18
  approximate_date: date | None = None
17
19
  comments: str | None = None
18
20
 
21
+ def _has_value(self, value: str | Coding | None) -> bool:
22
+ """Check if a value is set."""
23
+ if value is None:
24
+ return False
25
+ if isinstance(value, str):
26
+ return value.strip() != ""
27
+ # For Coding objects (dicts), check if it exists
28
+ return True
29
+
19
30
  def _get_error_details(self, method: str) -> list[InitErrorDetails]:
20
31
  errors = super()._get_error_details(method)
21
32
 
33
+ has_cpt_code = self._has_value(self.cpt_code)
34
+ has_cvx_code = self._has_value(self.cvx_code)
35
+
36
+ if has_cpt_code ^ has_cvx_code:
37
+ value = self.cpt_code if has_cpt_code else self.cvx_code
38
+ errors.append(
39
+ self._create_error_detail(
40
+ "value",
41
+ "Both cpt_code and cvx_code must be provided if one is specified and cannot be empty.",
42
+ value,
43
+ )
44
+ )
45
+
46
+ if self.unstructured and (has_cpt_code or has_cvx_code):
47
+ message = "Unstructured codes cannot be used with CPT or CVX codes."
48
+ errors.append(self._create_error_detail("value", message, self.cpt_code))
49
+
50
+ if isinstance(self.cpt_code, dict) and self.cpt_code["system"] != CodeSystems.CPT:
51
+ message = f"The 'cpt_code.system' field must be '{CodeSystems.CPT}'"
52
+ errors.append(self._create_error_detail("value", message, self.cpt_code))
53
+
54
+ if isinstance(self.cvx_code, dict) and self.cvx_code["system"] != CodeSystems.CVX:
55
+ message = f"The 'cvx_code.system' field must be '{CodeSystems.CVX}'"
56
+ errors.append(self._create_error_detail("value", message, self.cvx_code))
57
+
58
+ if self.unstructured and self.unstructured["system"] != CodeSystems.UNSTRUCTURED:
59
+ message = f"The 'unstructured.system' field must be '{CodeSystems.UNSTRUCTURED}'"
60
+ errors.append(self._create_error_detail("value", message, self.unstructured))
61
+
22
62
  if self.comments and len(self.comments) > 255:
23
63
  errors.append(
24
64
  self._create_error_detail(
@@ -1,6 +1,8 @@
1
1
  from pydantic import Field
2
+ from pydantic_core import InitErrorDetails
2
3
 
3
4
  from canvas_sdk.commands.base import _BaseCommand
5
+ from canvas_sdk.commands.constants import CodeSystems, Coding
4
6
 
5
7
 
6
8
  class MedicationStatementCommand(_BaseCommand):
@@ -9,11 +11,24 @@ class MedicationStatementCommand(_BaseCommand):
9
11
  class Meta:
10
12
  key = "medicationStatement"
11
13
 
12
- fdb_code: str | None = Field(
14
+ fdb_code: str | Coding | None = Field(
13
15
  default=None, json_schema_extra={"commands_api_name": "medication"}
14
16
  )
15
17
  sig: str | None = None
16
18
 
19
+ def _get_error_details(self, method: str) -> list[InitErrorDetails]:
20
+ errors = super()._get_error_details(method)
21
+
22
+ if (
23
+ isinstance(self.fdb_code, dict)
24
+ and self.fdb_code["system"] != CodeSystems.FDB
25
+ and self.fdb_code["system"] != CodeSystems.UNSTRUCTURED
26
+ ):
27
+ message = f"The 'coding.system' field must be '{CodeSystems.FDB}' or '{CodeSystems.UNSTRUCTURED}'."
28
+ errors.append(self._create_error_detail("value", message, self.fdb_code))
29
+
30
+ return errors
31
+
17
32
 
18
33
  # how do we make sure fdb_code is a valid code?
19
34
 
@@ -1,8 +1,10 @@
1
1
  from datetime import date
2
2
 
3
3
  from pydantic import Field
4
+ from pydantic_core import InitErrorDetails
4
5
 
5
6
  from canvas_sdk.commands.base import _BaseCommand as BaseCommand
7
+ from canvas_sdk.commands.constants import CodeSystems, Coding
6
8
 
7
9
 
8
10
  class PastSurgicalHistoryCommand(BaseCommand):
@@ -11,9 +13,22 @@ class PastSurgicalHistoryCommand(BaseCommand):
11
13
  class Meta:
12
14
  key = "surgicalHistory"
13
15
 
14
- past_surgical_history: str | None = None
16
+ past_surgical_history: str | Coding | None = None
15
17
  approximate_date: date | None = None
16
18
  comment: str | None = Field(max_length=1000, default=None)
17
19
 
20
+ def _get_error_details(self, method: str) -> list[InitErrorDetails]:
21
+ errors = super()._get_error_details(method)
22
+
23
+ if (
24
+ isinstance(self.past_surgical_history, dict)
25
+ and self.past_surgical_history["system"] != CodeSystems.SNOMED
26
+ and self.past_surgical_history["system"] != CodeSystems.UNSTRUCTURED
27
+ ):
28
+ message = f"The 'coding.system' field must be '{CodeSystems.SNOMED}' or '{CodeSystems.UNSTRUCTURED}'."
29
+ errors.append(self._create_error_detail("value", message, self.past_surgical_history))
30
+
31
+ return errors
32
+
18
33
 
19
34
  __exports__ = ("PastSurgicalHistoryCommand",)
@@ -1,6 +1,8 @@
1
1
  from pydantic import Field
2
+ from pydantic_core import InitErrorDetails
2
3
 
3
4
  from canvas_sdk.commands.base import _BaseCommand as BaseCommand
5
+ from canvas_sdk.commands.constants import CodeSystems, Coding
4
6
 
5
7
 
6
8
  class PerformCommand(BaseCommand):
@@ -9,8 +11,23 @@ class PerformCommand(BaseCommand):
9
11
  class Meta:
10
12
  key = "perform"
11
13
 
12
- cpt_code: str | None = Field(default=None, json_schema_extra={"commands_api_name": "perform"})
14
+ cpt_code: str | Coding | None = Field(
15
+ default=None, json_schema_extra={"commands_api_name": "perform"}
16
+ )
13
17
  notes: str | None = None
14
18
 
19
+ def _get_error_details(self, method: str) -> list[InitErrorDetails]:
20
+ errors = super()._get_error_details(method)
21
+
22
+ if (
23
+ isinstance(self.cpt_code, dict)
24
+ and self.cpt_code["system"] != CodeSystems.CPT
25
+ and self.cpt_code["system"] != CodeSystems.UNSTRUCTURED
26
+ ):
27
+ message = f"The 'coding.system' field must be '{CodeSystems.CPT}' or '{CodeSystems.UNSTRUCTURED}'."
28
+ errors.append(self._create_error_detail("value", message, self.cpt_code))
29
+
30
+ return errors
31
+
15
32
 
16
33
  __exports__ = ("PerformCommand",)
@@ -7,7 +7,7 @@ from typing import Any
7
7
  from pydantic import Field, conlist
8
8
  from pydantic_core import InitErrorDetails
9
9
 
10
- from canvas_sdk.commands.base import _BaseCommand, _SendableCommandMixin
10
+ from canvas_sdk.commands.base import _BaseCommand, _ReviewableCommandMixin, _SendableCommandMixin
11
11
  from canvas_sdk.commands.constants import ClinicalQuantity
12
12
  from canvas_sdk.effects import Effect
13
13
  from canvas_sdk.effects.compound_medications.compound_medication import (
@@ -37,7 +37,7 @@ class CompoundMedicationData:
37
37
  }
38
38
 
39
39
 
40
- class PrescribeCommand(_SendableCommandMixin, _BaseCommand):
40
+ class PrescribeCommand(_ReviewableCommandMixin, _SendableCommandMixin, _BaseCommand):
41
41
  """A class for managing a Prescribe command within a specific note."""
42
42
 
43
43
  class Meta:
@@ -74,12 +74,16 @@ class PrescribeCommand(_SendableCommandMixin, _BaseCommand):
74
74
  default=None, json_schema_extra={"commands_api_name": "compound_medication_data"}
75
75
  )
76
76
 
77
+ def _has_fdb_code(self) -> bool:
78
+ """Check if fdb_code is provided and non-empty."""
79
+ return self.fdb_code is not None and self.fdb_code.strip() != ""
80
+
77
81
  def _get_error_details(self, method: str) -> list[InitErrorDetails]:
78
82
  """Add compound medication validation to the base validation."""
79
83
  errors = super()._get_error_details(method)
80
84
 
81
85
  # Validate that exactly one medication type is provided
82
- has_fdb_code = self.fdb_code is not None and self.fdb_code.strip() != ""
86
+ has_fdb_code = self._has_fdb_code()
83
87
  has_compound_medication_id = (
84
88
  self.compound_medication_id is not None and self.compound_medication_id.strip() != ""
85
89
  )
@@ -156,10 +160,8 @@ class PrescribeCommand(_SendableCommandMixin, _BaseCommand):
156
160
  str(Decimal(self.quantity_to_dispense)) if self.quantity_to_dispense else None
157
161
  )
158
162
 
159
- values["compound_medication_values"] = {}
160
-
161
163
  if self.is_dirty("compound_medication_id") and self.compound_medication_id:
162
- values["compound_medication_values"]["id"] = values.pop("compound_medication_id")
164
+ values["compound_medication_values"] = {"id": values.pop("compound_medication_id")}
163
165
 
164
166
  # Handle compound medication data
165
167
  elif (
@@ -170,9 +172,6 @@ class PrescribeCommand(_SendableCommandMixin, _BaseCommand):
170
172
  if isinstance(compound_data, CompoundMedicationData):
171
173
  values["compound_medication_values"] = compound_data.to_dict()
172
174
 
173
- if values.get("fdb_code") is not None and values.get("compound_medication_values") == {}:
174
- del values["compound_medication_values"]
175
-
176
175
  return values
177
176
 
178
177
  def originate(self, line_number: int = -1) -> Effect:
@@ -1,4 +1,4 @@
1
- from typing import Literal
1
+ from typing import cast
2
2
 
3
3
  from django.db.models.expressions import Subquery
4
4
  from pydantic_core import InitErrorDetails
@@ -13,13 +13,13 @@ class RefillCommand(PrescribeCommand):
13
13
  class Meta:
14
14
  key = "refill"
15
15
 
16
- def _get_error_details(
17
- self, method: Literal["originate", "edit", "delete", "commit", "enter_in_error"]
18
- ) -> list[InitErrorDetails]:
16
+ def _get_error_details(self, method: str) -> list[InitErrorDetails]:
19
17
  errors = super()._get_error_details(method)
20
18
 
21
19
  if self.fdb_code:
22
- subquery = Subquery(Note.objects.filter(id=self.note_uuid).values("patient_id")[:1])
20
+ subquery = Subquery(
21
+ Note.objects.filter(id=cast(str, self.note_uuid)).values("patient_id")[:1]
22
+ )
23
23
  if (
24
24
  not Medication.objects.active()
25
25
  .filter(codings__code=self.fdb_code, patient=subquery)
@@ -1,4 +1,4 @@
1
- from typing import Literal
1
+ from typing import cast
2
2
  from uuid import UUID
3
3
 
4
4
  from django.db.models.expressions import Subquery
@@ -21,13 +21,13 @@ class ResolveConditionCommand(BaseCommand):
21
21
  show_in_condition_list: bool = False
22
22
  rationale: str | None = Field(max_length=1024, default=None)
23
23
 
24
- def _get_error_details(
25
- self, method: Literal["originate", "edit", "delete", "commit", "enter_in_error"]
26
- ) -> list[InitErrorDetails]:
24
+ def _get_error_details(self, method: str) -> list[InitErrorDetails]:
27
25
  errors = super()._get_error_details(method)
28
26
 
29
27
  if self.condition_id:
30
- subquery = Subquery(Note.objects.filter(id=self.note_uuid).values("patient_id")[:1])
28
+ subquery = Subquery(
29
+ Note.objects.filter(id=cast(str, self.note_uuid)).values("patient_id")[:1]
30
+ )
31
31
  if (
32
32
  not Condition.objects.active()
33
33
  .filter(id=self.condition_id, patient=subquery)
@@ -0,0 +1,3 @@
1
+ from canvas_sdk.commands.commands.review.base import ReportReviewCommunicationMethod, ReviewMode
2
+
3
+ __all__ = __exports__ = ("ReportReviewCommunicationMethod", "ReviewMode")