canvas 0.1.12__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 (56) hide show
  1. {canvas-0.1.12.dist-info → canvas-0.1.14.dist-info}/METADATA +42 -1
  2. canvas-0.1.14.dist-info/RECORD +90 -0
  3. canvas_cli/apps/plugin/__init__.py +3 -1
  4. canvas_cli/apps/plugin/plugin.py +74 -0
  5. canvas_cli/main.py +5 -3
  6. canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/protocols/my_protocol.py +3 -0
  7. canvas_generated/messages/effects_pb2.py +28 -0
  8. canvas_generated/messages/effects_pb2.pyi +147 -0
  9. canvas_generated/messages/effects_pb2_grpc.py +4 -0
  10. canvas_generated/messages/events_pb2.py +31 -0
  11. canvas_generated/messages/events_pb2.pyi +445 -0
  12. canvas_generated/messages/events_pb2_grpc.py +4 -0
  13. canvas_generated/messages/plugins_pb2.py +28 -0
  14. canvas_generated/messages/plugins_pb2.pyi +15 -0
  15. canvas_generated/messages/plugins_pb2_grpc.py +4 -0
  16. canvas_generated/services/plugin_runner_pb2.py +28 -0
  17. canvas_generated/services/plugin_runner_pb2.pyi +6 -0
  18. canvas_generated/services/plugin_runner_pb2_grpc.py +100 -0
  19. canvas_sdk/base.py +45 -0
  20. canvas_sdk/commands/base.py +61 -41
  21. canvas_sdk/commands/commands/assess.py +6 -2
  22. canvas_sdk/commands/commands/diagnose.py +4 -14
  23. canvas_sdk/commands/commands/goal.py +3 -2
  24. canvas_sdk/commands/commands/history_present_illness.py +2 -1
  25. canvas_sdk/commands/commands/medication_statement.py +6 -2
  26. canvas_sdk/commands/commands/plan.py +2 -1
  27. canvas_sdk/commands/commands/prescribe.py +24 -11
  28. canvas_sdk/commands/commands/questionnaire.py +6 -2
  29. canvas_sdk/commands/commands/reason_for_visit.py +13 -6
  30. canvas_sdk/commands/commands/stop_medication.py +6 -2
  31. canvas_sdk/commands/commands/update_goal.py +4 -1
  32. canvas_sdk/commands/tests/test_utils.py +31 -64
  33. canvas_sdk/commands/tests/tests.py +116 -65
  34. canvas_sdk/data/__init__.py +1 -0
  35. canvas_sdk/data/base.py +22 -0
  36. canvas_sdk/data/patient.py +6 -0
  37. canvas_sdk/data/staff.py +6 -0
  38. canvas_sdk/data/task.py +60 -0
  39. canvas_sdk/effects/__init__.py +1 -1
  40. canvas_sdk/effects/banner_alert/__init__.py +2 -0
  41. canvas_sdk/effects/banner_alert/add_banner_alert.py +49 -0
  42. canvas_sdk/effects/banner_alert/remove_banner_alert.py +20 -0
  43. canvas_sdk/effects/base.py +4 -6
  44. canvas_sdk/events/__init__.py +1 -1
  45. canvas_sdk/handlers/__init__.py +1 -0
  46. canvas_sdk/handlers/base.py +16 -0
  47. canvas_sdk/handlers/cron_task.py +35 -0
  48. canvas_sdk/protocols/base.py +2 -11
  49. canvas_sdk/utils/stats.py +27 -0
  50. logger/__init__.py +2 -0
  51. logger/logger.py +48 -0
  52. canvas-0.1.12.dist-info/RECORD +0 -66
  53. canvas_sdk/effects/banner_alert/banner_alert.py +0 -37
  54. canvas_sdk/effects/banner_alert/constants.py +0 -19
  55. {canvas-0.1.12.dist-info → canvas-0.1.14.dist-info}/WHEEL +0 -0
  56. {canvas-0.1.12.dist-info → canvas-0.1.14.dist-info}/entry_points.txt +0 -0
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
@@ -22,6 +22,17 @@ from canvas_sdk.commands import (
22
22
  from canvas_sdk.commands.constants import Coding
23
23
 
24
24
 
25
+ class MaskedValue:
26
+ def __init__(self, value):
27
+ self.value = value
28
+
29
+ def __repr__(self) -> str:
30
+ return "MaskedValue(********)"
31
+
32
+ def __str___(self) -> str:
33
+ return "*******"
34
+
35
+
25
36
  def get_field_type_unformatted(field_props: dict[str, Any]) -> str:
26
37
  if t := field_props.get("type"):
27
38
  return field_props.get("format") or t
@@ -77,35 +88,7 @@ def fake(
77
88
  return random.choice([e for e in getattr(Command, t)])
78
89
 
79
90
 
80
- def raises_missing_error(
81
- base: dict,
82
- Command: (
83
- AssessCommand
84
- | DiagnoseCommand
85
- | GoalCommand
86
- | HistoryOfPresentIllnessCommand
87
- | MedicationStatementCommand
88
- | PlanCommand
89
- | PrescribeCommand
90
- | QuestionnaireCommand
91
- | ReasonForVisitCommand
92
- | StopMedicationCommand
93
- ),
94
- field: str,
95
- ) -> None:
96
- err_kwargs = base.copy()
97
- err_kwargs.pop(field)
98
- with pytest.raises(ValidationError) as e:
99
- Command(**err_kwargs)
100
- err_msg = repr(e.value)
101
- assert (
102
- f"1 validation error for {Command.__name__}\n{field}\n Field required [type=missing"
103
- in err_msg
104
- )
105
-
106
-
107
- def raises_none_error(
108
- base: dict,
91
+ def raises_wrong_type_error(
109
92
  Command: (
110
93
  AssessCommand
111
94
  | DiagnoseCommand
@@ -122,21 +105,24 @@ def raises_none_error(
122
105
  ) -> None:
123
106
  field_props = Command.model_json_schema()["properties"][field]
124
107
  field_type = get_field_type(field_props)
108
+ wrong_field_type = "integer" if field_type == "string" else "string"
125
109
 
126
110
  with pytest.raises(ValidationError) as e1:
127
- err_kwargs = base | {field: None}
111
+ err_kwargs = {field: fake({"type": wrong_field_type}, Command)}
128
112
  Command(**err_kwargs)
129
113
  err_msg1 = repr(e1.value)
130
114
 
131
- valid_kwargs = base | {field: fake(field_props, Command)}
115
+ valid_kwargs = {field: fake(field_props, Command)}
132
116
  cmd = Command(**valid_kwargs)
117
+ err_value = fake({"type": wrong_field_type}, Command)
133
118
  with pytest.raises(ValidationError) as e2:
134
- setattr(cmd, field, None)
119
+ setattr(cmd, field, err_value)
135
120
  err_msg2 = repr(e2.value)
136
121
 
137
122
  assert f"1 validation error for {Command.__name__}\n{field}" in err_msg1
138
123
  assert f"1 validation error for {Command.__name__}\n{field}" in err_msg2
139
124
 
125
+ field_type = "dictionary" if field_type == "Coding" else field_type
140
126
  if field_type == "number":
141
127
  assert f"Input should be an instance of Decimal" in err_msg1
142
128
  assert f"Input should be an instance of Decimal" in err_msg2
@@ -148,8 +134,7 @@ def raises_none_error(
148
134
  assert f"Input should be a valid {field_type}" in err_msg2
149
135
 
150
136
 
151
- def raises_wrong_type_error(
152
- base: dict,
137
+ def raises_none_error_for_effect_method(
153
138
  Command: (
154
139
  AssessCommand
155
140
  | DiagnoseCommand
@@ -162,34 +147,16 @@ def raises_wrong_type_error(
162
147
  | ReasonForVisitCommand
163
148
  | StopMedicationCommand
164
149
  ),
165
- field: str,
150
+ method: str,
166
151
  ) -> None:
167
- field_props = Command.model_json_schema()["properties"][field]
168
- field_type = get_field_type(field_props)
169
- wrong_field_type = "integer" if field_type == "string" else "string"
170
-
171
- with pytest.raises(ValidationError) as e1:
172
- err_kwargs = base | {field: fake({"type": wrong_field_type}, Command)}
173
- Command(**err_kwargs)
174
- err_msg1 = repr(e1.value)
175
-
176
- valid_kwargs = base | {field: fake(field_props, Command)}
177
- cmd = Command(**valid_kwargs)
178
- err_value = fake({"type": wrong_field_type}, Command)
179
- with pytest.raises(ValidationError) as e2:
180
- setattr(cmd, field, err_value)
181
- err_msg2 = repr(e2.value)
182
-
183
- assert f"1 validation error for {Command.__name__}\n{field}" in err_msg1
184
- assert f"1 validation error for {Command.__name__}\n{field}" in err_msg2
185
-
186
- field_type = "dictionary" if field_type == "Coding" else field_type
187
- if field_type == "number":
188
- assert f"Input should be an instance of Decimal" in err_msg1
189
- assert f"Input should be an instance of Decimal" in err_msg2
190
- elif field_type[0].isupper():
191
- assert f"Input should be an instance of {Command.__name__}.{field_type}" in err_msg1
192
- assert f"Input should be an instance of {Command.__name__}.{field_type}" in err_msg2
193
- else:
194
- assert f"Input should be a valid {field_type}" in err_msg1
195
- assert f"Input should be a valid {field_type}" in err_msg2
152
+ cmd = Command()
153
+ method_required_fields = cmd._get_effect_method_required_fields(method)
154
+ with pytest.raises(ValidationError) as e:
155
+ getattr(cmd, method)()
156
+ e_msg = repr(e.value)
157
+ assert f"{len(method_required_fields)} validation errors for {Command.__name__}" in e_msg
158
+ for f in method_required_fields:
159
+ assert (
160
+ f"Field '{f}' is required to {method.replace('_', ' ')} a command [type=missing, input_value=None, input_type=NoneType]"
161
+ in e_msg
162
+ )