canvas 0.1.12b0__py3-none-any.whl → 0.1.14__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 (60) hide show
  1. {canvas-0.1.12b0.dist-info → canvas-0.1.14.dist-info}/METADATA +43 -2
  2. canvas-0.1.14.dist-info/RECORD +90 -0
  3. {canvas-0.1.12b0.dist-info → canvas-0.1.14.dist-info}/WHEEL +1 -1
  4. canvas_cli/apps/plugin/__init__.py +3 -1
  5. canvas_cli/apps/plugin/plugin.py +74 -0
  6. canvas_cli/main.py +5 -3
  7. canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/protocols/my_protocol.py +3 -0
  8. canvas_generated/messages/effects_pb2.py +28 -0
  9. canvas_generated/messages/effects_pb2.pyi +147 -0
  10. canvas_generated/messages/events_pb2.py +31 -0
  11. {generated → canvas_generated}/messages/events_pb2.pyi +3 -1
  12. {generated → canvas_generated}/messages/plugins_pb2.py +7 -7
  13. canvas_generated/services/plugin_runner_pb2.py +28 -0
  14. {generated → canvas_generated}/services/plugin_runner_pb2.pyi +2 -2
  15. {generated → canvas_generated}/services/plugin_runner_pb2_grpc.py +14 -14
  16. canvas_sdk/base.py +45 -0
  17. canvas_sdk/commands/base.py +61 -41
  18. canvas_sdk/commands/commands/assess.py +6 -2
  19. canvas_sdk/commands/commands/diagnose.py +4 -14
  20. canvas_sdk/commands/commands/goal.py +3 -2
  21. canvas_sdk/commands/commands/history_present_illness.py +2 -1
  22. canvas_sdk/commands/commands/medication_statement.py +6 -2
  23. canvas_sdk/commands/commands/plan.py +2 -1
  24. canvas_sdk/commands/commands/prescribe.py +24 -11
  25. canvas_sdk/commands/commands/questionnaire.py +6 -2
  26. canvas_sdk/commands/commands/reason_for_visit.py +13 -6
  27. canvas_sdk/commands/commands/stop_medication.py +6 -2
  28. canvas_sdk/commands/commands/update_goal.py +4 -1
  29. canvas_sdk/commands/tests/test_utils.py +31 -64
  30. canvas_sdk/commands/tests/tests.py +116 -65
  31. canvas_sdk/data/__init__.py +1 -0
  32. canvas_sdk/data/base.py +22 -0
  33. canvas_sdk/data/patient.py +6 -0
  34. canvas_sdk/data/staff.py +6 -0
  35. canvas_sdk/data/task.py +60 -0
  36. canvas_sdk/effects/__init__.py +1 -1
  37. canvas_sdk/effects/banner_alert/__init__.py +2 -0
  38. canvas_sdk/effects/banner_alert/add_banner_alert.py +49 -0
  39. canvas_sdk/effects/banner_alert/remove_banner_alert.py +20 -0
  40. canvas_sdk/effects/base.py +4 -6
  41. canvas_sdk/events/__init__.py +1 -1
  42. canvas_sdk/handlers/__init__.py +1 -0
  43. canvas_sdk/handlers/base.py +16 -0
  44. canvas_sdk/handlers/cron_task.py +35 -0
  45. canvas_sdk/protocols/base.py +2 -11
  46. canvas_sdk/utils/stats.py +27 -0
  47. logger/__init__.py +2 -0
  48. logger/logger.py +48 -0
  49. canvas-0.1.12b0.dist-info/RECORD +0 -78
  50. canvas_sdk/effects/banner_alert/banner_alert.py +0 -37
  51. canvas_sdk/effects/banner_alert/constants.py +0 -19
  52. generated/messages/effects_pb2.py +0 -28
  53. generated/messages/effects_pb2.pyi +0 -25
  54. generated/messages/events_pb2.py +0 -31
  55. generated/services/plugin_runner_pb2.py +0 -28
  56. {canvas-0.1.12b0.dist-info → canvas-0.1.14.dist-info}/entry_points.txt +0 -0
  57. {generated → canvas_generated}/messages/effects_pb2_grpc.py +0 -0
  58. {generated → canvas_generated}/messages/events_pb2_grpc.py +0 -0
  59. {generated → canvas_generated}/messages/plugins_pb2.pyi +0 -0
  60. {generated → canvas_generated}/messages/plugins_pb2_grpc.py +0 -0
@@ -2,8 +2,8 @@
2
2
  """Client and server classes corresponding to protobuf-defined services."""
3
3
  import grpc
4
4
 
5
- from generated.messages import events_pb2 as generated_dot_messages_dot_events__pb2
6
- from generated.messages import plugins_pb2 as generated_dot_messages_dot_plugins__pb2
5
+ from canvas_generated.messages import events_pb2 as canvas__generated_dot_messages_dot_events__pb2
6
+ from canvas_generated.messages import plugins_pb2 as canvas__generated_dot_messages_dot_plugins__pb2
7
7
 
8
8
 
9
9
  class PluginRunnerStub(object):
@@ -17,13 +17,13 @@ class PluginRunnerStub(object):
17
17
  """
18
18
  self.HandleEvent = channel.unary_stream(
19
19
  '/canvas.PluginRunner/HandleEvent',
20
- request_serializer=generated_dot_messages_dot_events__pb2.Event.SerializeToString,
21
- response_deserializer=generated_dot_messages_dot_events__pb2.EventResponse.FromString,
20
+ request_serializer=canvas__generated_dot_messages_dot_events__pb2.Event.SerializeToString,
21
+ response_deserializer=canvas__generated_dot_messages_dot_events__pb2.EventResponse.FromString,
22
22
  )
23
23
  self.ReloadPlugins = channel.unary_stream(
24
24
  '/canvas.PluginRunner/ReloadPlugins',
25
- request_serializer=generated_dot_messages_dot_plugins__pb2.ReloadPluginsRequest.SerializeToString,
26
- response_deserializer=generated_dot_messages_dot_plugins__pb2.ReloadPluginsResponse.FromString,
25
+ request_serializer=canvas__generated_dot_messages_dot_plugins__pb2.ReloadPluginsRequest.SerializeToString,
26
+ response_deserializer=canvas__generated_dot_messages_dot_plugins__pb2.ReloadPluginsResponse.FromString,
27
27
  )
28
28
 
29
29
 
@@ -47,13 +47,13 @@ def add_PluginRunnerServicer_to_server(servicer, server):
47
47
  rpc_method_handlers = {
48
48
  'HandleEvent': grpc.unary_stream_rpc_method_handler(
49
49
  servicer.HandleEvent,
50
- request_deserializer=generated_dot_messages_dot_events__pb2.Event.FromString,
51
- response_serializer=generated_dot_messages_dot_events__pb2.EventResponse.SerializeToString,
50
+ request_deserializer=canvas__generated_dot_messages_dot_events__pb2.Event.FromString,
51
+ response_serializer=canvas__generated_dot_messages_dot_events__pb2.EventResponse.SerializeToString,
52
52
  ),
53
53
  'ReloadPlugins': grpc.unary_stream_rpc_method_handler(
54
54
  servicer.ReloadPlugins,
55
- request_deserializer=generated_dot_messages_dot_plugins__pb2.ReloadPluginsRequest.FromString,
56
- response_serializer=generated_dot_messages_dot_plugins__pb2.ReloadPluginsResponse.SerializeToString,
55
+ request_deserializer=canvas__generated_dot_messages_dot_plugins__pb2.ReloadPluginsRequest.FromString,
56
+ response_serializer=canvas__generated_dot_messages_dot_plugins__pb2.ReloadPluginsResponse.SerializeToString,
57
57
  ),
58
58
  }
59
59
  generic_handler = grpc.method_handlers_generic_handler(
@@ -77,8 +77,8 @@ class PluginRunner(object):
77
77
  timeout=None,
78
78
  metadata=None):
79
79
  return grpc.experimental.unary_stream(request, target, '/canvas.PluginRunner/HandleEvent',
80
- generated_dot_messages_dot_events__pb2.Event.SerializeToString,
81
- generated_dot_messages_dot_events__pb2.EventResponse.FromString,
80
+ canvas__generated_dot_messages_dot_events__pb2.Event.SerializeToString,
81
+ canvas__generated_dot_messages_dot_events__pb2.EventResponse.FromString,
82
82
  options, channel_credentials,
83
83
  insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
84
84
 
@@ -94,7 +94,7 @@ class PluginRunner(object):
94
94
  timeout=None,
95
95
  metadata=None):
96
96
  return grpc.experimental.unary_stream(request, target, '/canvas.PluginRunner/ReloadPlugins',
97
- generated_dot_messages_dot_plugins__pb2.ReloadPluginsRequest.SerializeToString,
98
- generated_dot_messages_dot_plugins__pb2.ReloadPluginsResponse.FromString,
97
+ canvas__generated_dot_messages_dot_plugins__pb2.ReloadPluginsRequest.SerializeToString,
98
+ canvas__generated_dot_messages_dot_plugins__pb2.ReloadPluginsResponse.FromString,
99
99
  options, channel_credentials,
100
100
  insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
canvas_sdk/base.py ADDED
@@ -0,0 +1,45 @@
1
+ from datetime import date, datetime
2
+ from enum import Enum
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, ConfigDict
6
+ from pydantic_core import InitErrorDetails, PydanticCustomError, ValidationError
7
+
8
+
9
+ class Model(BaseModel):
10
+ """A base model that includes validation methods."""
11
+
12
+ class Meta:
13
+ pass
14
+
15
+ model_config = ConfigDict(
16
+ strict=True,
17
+ revalidate_instances="always",
18
+ validate_assignment=True,
19
+ json_encoders={
20
+ date: lambda v: v.isoformat(),
21
+ datetime: lambda v: v.isoformat(),
22
+ Enum: lambda v: v.value,
23
+ },
24
+ )
25
+
26
+ def _get_effect_method_required_fields(self, method: str) -> tuple[str]:
27
+ return getattr(self.Meta, f"{method}_required_fields", tuple())
28
+
29
+ def _create_error_detail(self, type: str, message: str, value: Any) -> InitErrorDetails:
30
+ return InitErrorDetails({"type": PydanticCustomError(type, message), "input": value})
31
+
32
+ def _get_error_details(self, method: str) -> list[InitErrorDetails]:
33
+ required_fields = self._get_effect_method_required_fields(method)
34
+ return [
35
+ self._create_error_detail(
36
+ "missing", f"Field '{field}' is required to {method.replace('_', ' ')} a command", v
37
+ )
38
+ for field in required_fields
39
+ if (v := getattr(self, field)) is None
40
+ ]
41
+
42
+ def _validate_before_effect(self, method: str) -> None:
43
+ self.model_validate(self)
44
+ if error_details := self._get_error_details(method):
45
+ raise ValidationError.from_exception_data(self.__class__.__name__, error_details)
@@ -1,29 +1,51 @@
1
1
  import json
2
+ import re
2
3
  from enum import EnumType
3
- from typing import get_args
4
+ from typing import Literal, get_args
4
5
 
5
- from pydantic import BaseModel, ConfigDict, model_validator
6
- from typing_extensions import Self
6
+ from canvas_sdk.base import Model
7
+ from canvas_sdk.effects import Effect, EffectType
7
8
 
8
- from canvas_sdk.effects import Effect
9
-
10
-
11
- class _BaseCommand(BaseModel):
12
- model_config = ConfigDict(strict=True, validate_assignment=True)
13
9
 
10
+ class _BaseCommand(Model):
14
11
  class Meta:
15
12
  key = ""
16
-
17
- # todo: update int to str as we should use external identifiers
18
- note_id: int | None = None
13
+ originate_required_fields = (
14
+ "user_id",
15
+ "note_uuid",
16
+ )
17
+ edit_required_fields = (
18
+ "user_id",
19
+ "command_uuid",
20
+ )
21
+ delete_required_fields = (
22
+ "user_id",
23
+ "command_uuid",
24
+ )
25
+ commit_required_fields = (
26
+ "user_id",
27
+ "command_uuid",
28
+ )
29
+ enter_in_error_required_fields = (
30
+ "user_id",
31
+ "command_uuid",
32
+ )
33
+
34
+ def constantized_key(self) -> str:
35
+ return re.sub(r"(?<!^)(?=[A-Z])", "_", self.Meta.key).upper()
36
+
37
+ note_uuid: str | None = None
19
38
  command_uuid: str | None = None
20
- user_id: int
39
+ user_id: int | None = None
21
40
 
22
- @model_validator(mode="after")
23
- def _verify_has_note_id_or_command_id(self) -> Self:
24
- if not self.note_id and not self.command_uuid:
25
- raise ValueError("Command should have either a note_id or a command_uuid.")
26
- return self
41
+ def _get_effect_method_required_fields(
42
+ self, method: Literal["originate", "edit", "delete", "commit", "enter_in_error"]
43
+ ) -> tuple[str]:
44
+ base_required_fields: tuple = getattr(
45
+ _BaseCommand.Meta, f"{method}_required_fields", tuple()
46
+ )
47
+ command_required_fields = super()._get_effect_method_required_fields(method)
48
+ return tuple(set(base_required_fields) | set(command_required_fields))
27
49
 
28
50
  @property
29
51
  def values(self) -> dict:
@@ -52,11 +74,12 @@ class _BaseCommand(BaseModel):
52
74
  @classmethod
53
75
  def command_schema(cls) -> dict:
54
76
  """The schema of the command."""
55
- base_properties = {"note_id", "command_uuid", "user_id"}
77
+ base_properties = {"note_uuid", "command_uuid", "user_id"}
56
78
  schema = cls.model_json_schema()
79
+ required_fields: tuple = getattr(cls.Meta, "originate_required_fields", tuple())
57
80
  return {
58
81
  definition.get("commands_api_name", name): {
59
- "required": name in schema["required"],
82
+ "required": name in required_fields,
60
83
  "type": cls._get_property_type(name),
61
84
  "choices": cls._get_property_choices(name, schema),
62
85
  }
@@ -66,23 +89,23 @@ class _BaseCommand(BaseModel):
66
89
 
67
90
  def originate(self) -> Effect:
68
91
  """Originate a new command in the note body."""
69
- if not self.note_id:
70
- raise AttributeError("Note id is required to originate a command")
71
- return {
72
- "type": f"ADD_{self.Meta.key.upper()}_COMMAND",
73
- "payload": {
74
- "user": self.user_id,
75
- "note": self.note_id,
76
- "data": self.values,
77
- },
78
- }
92
+ self._validate_before_effect("originate")
93
+ return Effect(
94
+ type=EffectType.Value(f"ORIGINATE_{self.constantized_key()}_COMMAND"),
95
+ payload=json.dumps(
96
+ {
97
+ "user": self.user_id,
98
+ "note": self.note_uuid,
99
+ "data": self.values,
100
+ }
101
+ ),
102
+ )
79
103
 
80
104
  def edit(self) -> Effect:
81
105
  """Edit the command."""
82
- if not self.command_uuid:
83
- raise AttributeError("Command uuid is required to edit a command")
106
+ self._validate_before_effect("edit")
84
107
  return {
85
- "type": f"EDIT_{self.Meta.key.upper()}_COMMAND",
108
+ "type": f"EDIT_{self.constantized_key()}_COMMAND",
86
109
  "payload": {
87
110
  "user": self.user_id,
88
111
  "command": self.command_uuid,
@@ -92,27 +115,24 @@ class _BaseCommand(BaseModel):
92
115
 
93
116
  def delete(self) -> Effect:
94
117
  """Delete the command."""
95
- if not self.command_uuid:
96
- raise AttributeError("Command uuid is required to delete a command")
118
+ self._validate_before_effect("delete")
97
119
  return {
98
- "type": f"DELETE_{self.Meta.key.upper()}_COMMAND",
120
+ "type": f"DELETE_{self.constantized_key()}_COMMAND",
99
121
  "payload": {"command": self.command_uuid, "user": self.user_id},
100
122
  }
101
123
 
102
124
  def commit(self) -> Effect:
103
125
  """Commit the command."""
104
- if not self.command_uuid:
105
- raise AttributeError("Command uuid is required to commit a command")
126
+ self._validate_before_effect("commit")
106
127
  return {
107
- "type": f"COMMIT_{self.Meta.key.upper()}_COMMAND",
128
+ "type": f"COMMIT_{self.constantized_key()}_COMMAND",
108
129
  "payload": {"command": self.command_uuid, "user": self.user_id},
109
130
  }
110
131
 
111
132
  def enter_in_error(self) -> Effect:
112
133
  """Mark the command as entered-in-error."""
113
- if not self.command_uuid:
114
- raise AttributeError("Command uuid is required to enter in error a command")
134
+ self._validate_before_effect("enter_in_error")
115
135
  return {
116
- "type": f"ENTER_IN_ERROR_{self.Meta.key.upper()}_COMMAND",
136
+ "type": f"ENTER_IN_ERROR_{self.constantized_key()}_COMMAND",
117
137
  "payload": {"command": self.command_uuid, "user": self.user_id},
118
138
  }
@@ -1,21 +1,25 @@
1
1
  from enum import Enum
2
2
 
3
- from canvas_sdk.commands.base import _BaseCommand
4
3
  from pydantic import Field
5
4
 
5
+ from canvas_sdk.commands.base import _BaseCommand
6
+
6
7
 
7
8
  class AssessCommand(_BaseCommand):
8
9
  """A class for managing an Assess command within a specific note."""
9
10
 
10
11
  class Meta:
11
12
  key = "assess"
13
+ originate_required_fields = ("condition_id",)
12
14
 
13
15
  class Status(Enum):
14
16
  IMPROVED = "improved"
15
17
  STABLE = "stable"
16
18
  DETERIORATED = "deteriorated"
17
19
 
18
- condition_id: str = Field(json_schema_extra={"commands_api_name": "condition"})
20
+ condition_id: str | None = Field(
21
+ default=None, json_schema_extra={"commands_api_name": "condition"}
22
+ )
19
23
  background: str | None = None
20
24
  status: Status | None = None
21
25
  narrative: str | None = None
@@ -10,8 +10,11 @@ class DiagnoseCommand(_BaseCommand):
10
10
 
11
11
  class Meta:
12
12
  key = "diagnose"
13
+ originate_required_fields = ("icd10_code",)
13
14
 
14
- icd10_code: str = Field(json_schema_extra={"commands_api_name": "diagnose"})
15
+ icd10_code: str | None = Field(
16
+ default=None, json_schema_extra={"commands_api_name": "diagnose"}
17
+ )
15
18
  background: str | None = None
16
19
  approximate_date_of_onset: datetime | None = None
17
20
  today_assessment: str | None = None
@@ -29,16 +32,3 @@ class DiagnoseCommand(_BaseCommand):
29
32
  ),
30
33
  "today_assessment": self.today_assessment,
31
34
  }
32
-
33
-
34
- # how do we make sure icd10_code is a valid code?
35
-
36
- # idea1:
37
- # create an auto-generated enum class of all possible icd10s, then type the field as that enum
38
- # will require releasing a new version with the new codes every year, and devs will need to update
39
- # to make sure they have the latest version to get the right set of codes.
40
-
41
- # idea2:
42
- # see if we can get ValueSets to play nicely with pydantic
43
-
44
- # idea3: runtime warning after pinging ontologies
@@ -9,6 +9,7 @@ class GoalCommand(_BaseCommand):
9
9
 
10
10
  class Meta:
11
11
  key = "goal"
12
+ originate_required_fields = ("goal_statement", "start_date")
12
13
 
13
14
  class Priority(Enum):
14
15
  HIGH = "high-priority"
@@ -26,7 +27,7 @@ class GoalCommand(_BaseCommand):
26
27
  NO_PROGRESS = "no-progress"
27
28
  NOT_ATTAINABLE = "not-attainable"
28
29
 
29
- goal_statement: str
30
+ goal_statement: str | None = None
30
31
  start_date: datetime | None = None
31
32
  due_date: datetime | None = None
32
33
  achievement_status: AchievementStatus | None = None
@@ -39,7 +40,7 @@ class GoalCommand(_BaseCommand):
39
40
  return {
40
41
  "goal_statement": self.goal_statement,
41
42
  "start_date": (self.start_date.isoformat() if self.start_date else None),
42
- "due_date": (self.due_date.isoformat() if self.start_date else None),
43
+ "due_date": (self.due_date.isoformat() if self.due_date else None),
43
44
  "achievement_status": (
44
45
  self.achievement_status.value if self.achievement_status else None
45
46
  ),
@@ -6,8 +6,9 @@ class HistoryOfPresentIllnessCommand(_BaseCommand):
6
6
 
7
7
  class Meta:
8
8
  key = "hpi"
9
+ originate_required_fields = ("narrative",)
9
10
 
10
- narrative: str
11
+ narrative: str | None = None
11
12
 
12
13
  @property
13
14
  def values(self) -> dict:
@@ -1,14 +1,18 @@
1
- from canvas_sdk.commands.base import _BaseCommand
2
1
  from pydantic import Field
3
2
 
3
+ from canvas_sdk.commands.base import _BaseCommand
4
+
4
5
 
5
6
  class MedicationStatementCommand(_BaseCommand):
6
7
  """A class for managing a MedicationStatement command within a specific note."""
7
8
 
8
9
  class Meta:
9
10
  key = "medicationStatement"
11
+ originate_required_fields = ("fdb_code",)
10
12
 
11
- fdb_code: str = Field(json_schema_extra={"commands_api_name": "medication"})
13
+ fdb_code: str | None = Field(
14
+ default=None, json_schema_extra={"commands_api_name": "medication"}
15
+ )
12
16
  sig: str | None = None
13
17
 
14
18
  @property
@@ -6,8 +6,9 @@ class PlanCommand(_BaseCommand):
6
6
 
7
7
  class Meta:
8
8
  key = "plan"
9
+ originate_required_fields = ("narrative",)
9
10
 
10
- narrative: str
11
+ narrative: str | None = None
11
12
 
12
13
  @property
13
14
  def values(self) -> dict:
@@ -11,23 +11,34 @@ class PrescribeCommand(_BaseCommand):
11
11
 
12
12
  class Meta:
13
13
  key = "prescribe"
14
+ originate_required_fields = (
15
+ "fdb_code",
16
+ "sig",
17
+ "quantity_to_dispense",
18
+ "type_to_dispense",
19
+ "refills",
20
+ "substitutions",
21
+ "prescriber_id",
22
+ )
14
23
 
15
24
  class Substitutions(Enum):
16
25
  ALLOWED = "allowed"
17
26
  NOT_ALLOWED = "not_allowed"
18
27
 
19
- fdb_code: str = Field(json_schema_extra={"commands_api_name": "prescribe"})
28
+ fdb_code: str | None = Field(default=None, json_schema_extra={"commands_api_name": "prescribe"})
20
29
  icd10_codes: list[str] | None = Field(
21
- None, json_schema_extra={"commandsd_api_name": "indications"}
30
+ None, json_schema_extra={"commands_api_name": "indications"}
22
31
  )
23
- sig: str
32
+ sig: str | None = None
24
33
  days_supply: int | None = None
25
- quantity_to_dispense: Decimal
26
- type_to_dispense: str
27
- refills: int
28
- substitutions: Substitutions = Substitutions.ALLOWED # type: ignore
34
+ quantity_to_dispense: Decimal | float | int | None = None
35
+ type_to_dispense: str | None = None
36
+ refills: int | None = None
37
+ substitutions: Substitutions | None = None
29
38
  pharmacy: str | None = None
30
- prescriber_id: str = Field(json_schema_extra={"commands_api_name": "prescriber"})
39
+ prescriber_id: str | None = Field(
40
+ default=None, json_schema_extra={"commands_api_name": "prescriber"}
41
+ )
31
42
  note_to_pharmacist: str | None = None
32
43
 
33
44
  @property
@@ -38,10 +49,12 @@ class PrescribeCommand(_BaseCommand):
38
49
  "icd10_codes": self.icd10_codes,
39
50
  "sig": self.sig,
40
51
  "days_supply": self.days_supply,
41
- "quantity_to_dispense": self.quantity_to_dispense,
42
- "type_to_dispense": self.type_to_dispense,
52
+ "quantity_to_dispense": (
53
+ str(Decimal(self.quantity_to_dispense)) if self.quantity_to_dispense else None
54
+ ),
55
+ # "type_to_dispense": self.type_to_dispense,
43
56
  "refills": self.refills,
44
- "substitutions": self.substitutions,
57
+ "substitutions": self.substitutions.value if self.substitutions else None,
45
58
  "pharmacy": self.pharmacy,
46
59
  "prescriber_id": self.prescriber_id,
47
60
  "note_to_pharmacist": self.note_to_pharmacist,
@@ -1,14 +1,18 @@
1
- from canvas_sdk.commands.base import _BaseCommand
2
1
  from pydantic import Field
3
2
 
3
+ from canvas_sdk.commands.base import _BaseCommand
4
+
4
5
 
5
6
  class QuestionnaireCommand(_BaseCommand):
6
7
  """A class for managing a Questionnaire command within a specific note."""
7
8
 
8
9
  class Meta:
9
10
  key = "questionnaire"
11
+ originate_required_fields = ("questionnaire_id",)
10
12
 
11
- questionnaire_id: str = Field(json_schema_extra={"commands_api_name": "questionnaire"})
13
+ questionnaire_id: str | None = Field(
14
+ default=None, json_schema_extra={"commands_api_name": "questionnaire"}
15
+ )
12
16
  result: str | None = None
13
17
 
14
18
  @property
@@ -1,5 +1,6 @@
1
- from pydantic import model_validator
2
- from typing_extensions import Self
1
+ from typing import Literal
2
+
3
+ from pydantic_core import InitErrorDetails
3
4
 
4
5
  from canvas_sdk.commands.base import _BaseCommand
5
6
  from canvas_sdk.commands.constants import Coding
@@ -16,11 +17,17 @@ class ReasonForVisitCommand(_BaseCommand):
16
17
  coding: Coding | None = None
17
18
  comment: str | None = None
18
19
 
19
- @model_validator(mode="after")
20
- def _verify_structured_has_a_coding(self) -> Self:
20
+ def _get_error_details(
21
+ self, method: Literal["originate", "edit", "delete", "commit", "enter_in_error"]
22
+ ) -> list[InitErrorDetails]:
23
+ errors = super()._get_error_details(method)
21
24
  if self.structured and not self.coding:
22
- raise ValueError("Structured RFV should have a coding.")
23
- return self
25
+ errors.append(
26
+ self._create_error_detail(
27
+ "value", f"Structured RFV should have a coding.", self.coding
28
+ )
29
+ )
30
+ return errors
24
31
 
25
32
  @classmethod
26
33
  def command_schema(cls) -> dict:
@@ -1,15 +1,19 @@
1
- from canvas_sdk.commands.base import _BaseCommand
2
1
  from pydantic import Field
3
2
 
3
+ from canvas_sdk.commands.base import _BaseCommand
4
+
4
5
 
5
6
  class StopMedicationCommand(_BaseCommand):
6
7
  """A class for managing a StopMedication command within a specific note."""
7
8
 
8
9
  class Meta:
9
10
  key = "stopMedication"
11
+ originate_required_fields = ("medication_id",)
10
12
 
11
13
  # how do we make sure this is a valid medication_id for the patient?
12
- medication_id: str = Field(json_schema_extra={"commands_api_name": "medication"})
14
+ medication_id: str | None = Field(
15
+ default=None, json_schema_extra={"commands_api_name": "medication"}
16
+ )
13
17
  rationale: str | None = None
14
18
 
15
19
  @property
@@ -11,6 +11,7 @@ class UpdateGoalCommand(_BaseCommand):
11
11
 
12
12
  class Meta:
13
13
  key = "updateGoal"
14
+ originate_required_fields = ("goal_id",)
14
15
 
15
16
  class AchievementStatus(Enum):
16
17
  IN_PROGRESS = "in-progress"
@@ -28,7 +29,9 @@ class UpdateGoalCommand(_BaseCommand):
28
29
  MEDIUM = "medium-priority"
29
30
  LOW = "low-priority"
30
31
 
31
- goal_id: str = Field(json_schema_extra={"commands_api_name": "goal_statement"})
32
+ goal_id: str | None = Field(
33
+ default=None, json_schema_extra={"commands_api_name": "goal_statement"}
34
+ )
32
35
  due_date: datetime | None = None
33
36
  achievement_status: AchievementStatus | None = None
34
37
  priority: Priority | None = None