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.
- {canvas-0.1.12.dist-info → canvas-0.1.14.dist-info}/METADATA +42 -1
- canvas-0.1.14.dist-info/RECORD +90 -0
- canvas_cli/apps/plugin/__init__.py +3 -1
- canvas_cli/apps/plugin/plugin.py +74 -0
- canvas_cli/main.py +5 -3
- canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/protocols/my_protocol.py +3 -0
- canvas_generated/messages/effects_pb2.py +28 -0
- canvas_generated/messages/effects_pb2.pyi +147 -0
- canvas_generated/messages/effects_pb2_grpc.py +4 -0
- canvas_generated/messages/events_pb2.py +31 -0
- canvas_generated/messages/events_pb2.pyi +445 -0
- canvas_generated/messages/events_pb2_grpc.py +4 -0
- canvas_generated/messages/plugins_pb2.py +28 -0
- canvas_generated/messages/plugins_pb2.pyi +15 -0
- canvas_generated/messages/plugins_pb2_grpc.py +4 -0
- canvas_generated/services/plugin_runner_pb2.py +28 -0
- canvas_generated/services/plugin_runner_pb2.pyi +6 -0
- canvas_generated/services/plugin_runner_pb2_grpc.py +100 -0
- canvas_sdk/base.py +45 -0
- canvas_sdk/commands/base.py +61 -41
- canvas_sdk/commands/commands/assess.py +6 -2
- canvas_sdk/commands/commands/diagnose.py +4 -14
- canvas_sdk/commands/commands/goal.py +3 -2
- canvas_sdk/commands/commands/history_present_illness.py +2 -1
- canvas_sdk/commands/commands/medication_statement.py +6 -2
- canvas_sdk/commands/commands/plan.py +2 -1
- canvas_sdk/commands/commands/prescribe.py +24 -11
- canvas_sdk/commands/commands/questionnaire.py +6 -2
- canvas_sdk/commands/commands/reason_for_visit.py +13 -6
- canvas_sdk/commands/commands/stop_medication.py +6 -2
- canvas_sdk/commands/commands/update_goal.py +4 -1
- canvas_sdk/commands/tests/test_utils.py +31 -64
- canvas_sdk/commands/tests/tests.py +116 -65
- canvas_sdk/data/__init__.py +1 -0
- canvas_sdk/data/base.py +22 -0
- canvas_sdk/data/patient.py +6 -0
- canvas_sdk/data/staff.py +6 -0
- canvas_sdk/data/task.py +60 -0
- canvas_sdk/effects/__init__.py +1 -1
- canvas_sdk/effects/banner_alert/__init__.py +2 -0
- canvas_sdk/effects/banner_alert/add_banner_alert.py +49 -0
- canvas_sdk/effects/banner_alert/remove_banner_alert.py +20 -0
- canvas_sdk/effects/base.py +4 -6
- canvas_sdk/events/__init__.py +1 -1
- canvas_sdk/handlers/__init__.py +1 -0
- canvas_sdk/handlers/base.py +16 -0
- canvas_sdk/handlers/cron_task.py +35 -0
- canvas_sdk/protocols/base.py +2 -11
- canvas_sdk/utils/stats.py +27 -0
- logger/__init__.py +2 -0
- logger/logger.py +48 -0
- canvas-0.1.12.dist-info/RECORD +0 -66
- canvas_sdk/effects/banner_alert/banner_alert.py +0 -37
- canvas_sdk/effects/banner_alert/constants.py +0 -19
- {canvas-0.1.12.dist-info → canvas-0.1.14.dist-info}/WHEEL +0 -0
- {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)
|
canvas_sdk/commands/base.py
CHANGED
|
@@ -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
|
|
6
|
-
from
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 = {"
|
|
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
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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(
|
|
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.
|
|
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
|
),
|
|
@@ -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(
|
|
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
|
|
@@ -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={"
|
|
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
|
|
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(
|
|
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":
|
|
42
|
-
|
|
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(
|
|
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
|
|
2
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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 =
|
|
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 =
|
|
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,
|
|
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
|
|
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
|
-
|
|
150
|
+
method: str,
|
|
166
151
|
) -> None:
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
+
)
|