Skip to content

Commit

Permalink
add custom validation for each type of effect
Browse files Browse the repository at this point in the history
  • Loading branch information
mbiannaccone committed Jul 10, 2024
1 parent 2a3ecb5 commit b7315c8
Show file tree
Hide file tree
Showing 12 changed files with 125 additions and 48 deletions.
77 changes: 56 additions & 21 deletions canvas_sdk/commands/base.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,73 @@
import json
import re
from enum import EnumType
from typing import get_args
from typing import Any, Literal, get_args

from pydantic import BaseModel, ConfigDict, model_validator
from typing_extensions import Self
from pydantic import BaseModel, ConfigDict
from pydantic_core import InitErrorDetails, PydanticCustomError, ValidationError

from canvas_sdk.effects import Effect, EffectType


class _BaseCommand(BaseModel):
model_config = ConfigDict(strict=True, validate_assignment=True)
model_config = ConfigDict(strict=True, revalidate_instances="always")

class Meta:
key = ""
originate_required_fields = (
"user_id",
"note_uuid",
)
edit_required_fields = (
"user_id",
"command_uuid",
)
delete_required_fields = (
"user_id",
"command_uuid",
)
commit_required_fields = (
"user_id",
"command_uuid",
)
enter_in_error_required_fields = (
"user_id",
"command_uuid",
)

def constantized_key(self) -> str:
return re.sub(r"(?<!^)(?=[A-Z])", "_", self.Meta.key).upper()

# todo: update int to str as we should use external identifiers
note_uuid: str | None = None
command_uuid: str | None = None
user_id: int
user_id: int | None = None

def _create_error_detail(self, type: str, message: str, value: Any) -> InitErrorDetails:
return InitErrorDetails({"type": PydanticCustomError(type, message), "input": value})

@model_validator(mode="after")
def _verify_has_note_uuid_or_command_id(self) -> Self:
if not self.note_uuid and not self.command_uuid:
raise ValueError("Command should have either a note_uuid or a command_uuid.")
return self
def _get_error_details(
self, method: Literal["originate", "edit", "delete", "commit", "enter_in_error"]
) -> list[InitErrorDetails]:
base_required_fields: tuple = getattr(
_BaseCommand.Meta, f"{method}_required_fields", tuple()
)
command_required_fields: tuple = getattr(self.Meta, f"{method}_required_fields", tuple())
required_fields = tuple(set(base_required_fields) | set(command_required_fields))

return [
self._create_error_detail(
"missing", f"Field '{field}' is required to {method.replace('_', ' ')} a command", v
)
for field in required_fields
if (v := getattr(self, field)) is None
]

def _validate_before_effect(
self, method: Literal["originate", "edit", "delete", "commit", "enter_in_error"]
) -> None:
self.model_validate(self)
if error_details := self._get_error_details(method):
raise ValidationError.from_exception_data(self.__class__.__name__, error_details)

@property
def values(self) -> dict:
Expand Down Expand Up @@ -70,8 +110,7 @@ def command_schema(cls) -> dict:

def originate(self) -> Effect:
"""Originate a new command in the note body."""
if not self.note_uuid:
raise AttributeError("Note id is required to originate a command")
self._validate_before_effect("originate")
return Effect(
type=EffectType.Value(f"ORIGINATE_{self.constantized_key()}_COMMAND"),
payload=json.dumps(
Expand All @@ -85,8 +124,7 @@ def originate(self) -> Effect:

def edit(self) -> Effect:
"""Edit the command."""
if not self.command_uuid:
raise AttributeError("Command uuid is required to edit a command")
self._validate_before_effect("edit")
return {
"type": f"EDIT_{self.constantized_key()}_COMMAND",
"payload": {
Expand All @@ -98,26 +136,23 @@ def edit(self) -> Effect:

def delete(self) -> Effect:
"""Delete the command."""
if not self.command_uuid:
raise AttributeError("Command uuid is required to delete a command")
self._validate_before_effect("delete")
return {
"type": f"DELETE_{self.constantized_key()}_COMMAND",
"payload": {"command": self.command_uuid, "user": self.user_id},
}

def commit(self) -> Effect:
"""Commit the command."""
if not self.command_uuid:
raise AttributeError("Command uuid is required to commit a command")
self._validate_before_effect("commit")
return {
"type": f"COMMIT_{self.constantized_key()}_COMMAND",
"payload": {"command": self.command_uuid, "user": self.user_id},
}

def enter_in_error(self) -> Effect:
"""Mark the command as entered-in-error."""
if not self.command_uuid:
raise AttributeError("Command uuid is required to enter in error a command")
self._validate_before_effect("enter_in_error")
return {
"type": f"ENTER_IN_ERROR_{self.constantized_key()}_COMMAND",
"payload": {"command": self.command_uuid, "user": self.user_id},
Expand Down
8 changes: 6 additions & 2 deletions canvas_sdk/commands/commands/assess.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
from enum import Enum

from canvas_sdk.commands.base import _BaseCommand
from pydantic import Field

from canvas_sdk.commands.base import _BaseCommand


class AssessCommand(_BaseCommand):
"""A class for managing an Assess command within a specific note."""

class Meta:
key = "assess"
originate_required_fields = ("condition_id",)

class Status(Enum):
IMPROVED = "improved"
STABLE = "stable"
DETERIORATED = "deteriorated"

condition_id: str = Field(json_schema_extra={"commands_api_name": "condition"})
condition_id: str | None = Field(
default=None, json_schema_extra={"commands_api_name": "condition"}
)
background: str | None = None
status: Status | None = None
narrative: str | None = None
Expand Down
5 changes: 4 additions & 1 deletion canvas_sdk/commands/commands/diagnose.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ class DiagnoseCommand(_BaseCommand):

class Meta:
key = "diagnose"
originate_required_fields = ("icd10_code",)

icd10_code: str = Field(json_schema_extra={"commands_api_name": "diagnose"})
icd10_code: str | None = Field(
default=None, json_schema_extra={"commands_api_name": "diagnose"}
)
background: str | None = None
approximate_date_of_onset: datetime | None = None
today_assessment: str | None = None
Expand Down
3 changes: 2 additions & 1 deletion canvas_sdk/commands/commands/goal.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class GoalCommand(_BaseCommand):

class Meta:
key = "goal"
originate_required_fields = ("goal_statement",)

class Priority(Enum):
HIGH = "high-priority"
Expand All @@ -26,7 +27,7 @@ class AchievementStatus(Enum):
NO_PROGRESS = "no-progress"
NOT_ATTAINABLE = "not-attainable"

goal_statement: str
goal_statement: str | None = None
start_date: datetime | None = None
due_date: datetime | None = None
achievement_status: AchievementStatus | None = None
Expand Down
3 changes: 2 additions & 1 deletion canvas_sdk/commands/commands/history_present_illness.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ class HistoryOfPresentIllnessCommand(_BaseCommand):

class Meta:
key = "hpi"
originate_required_fields = ("narrative",)

narrative: str
narrative: str | None

@property
def values(self) -> dict:
Expand Down
8 changes: 6 additions & 2 deletions canvas_sdk/commands/commands/medication_statement.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
from canvas_sdk.commands.base import _BaseCommand
from pydantic import Field

from canvas_sdk.commands.base import _BaseCommand


class MedicationStatementCommand(_BaseCommand):
"""A class for managing a MedicationStatement command within a specific note."""

class Meta:
key = "medicationStatement"
originate_required_fields = ("fdb_code",)

fdb_code: str = Field(json_schema_extra={"commands_api_name": "medication"})
fdb_code: str | None = Field(
default=None, json_schema_extra={"commands_api_name": "medication"}
)
sig: str | None = None

@property
Expand Down
3 changes: 2 additions & 1 deletion canvas_sdk/commands/commands/plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ class PlanCommand(_BaseCommand):

class Meta:
key = "plan"
originate_required_fields = ("narrative",)

narrative: str
narrative: str | None = None

@property
def values(self) -> dict:
Expand Down
26 changes: 18 additions & 8 deletions canvas_sdk/commands/commands/prescribe.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,33 @@ class PrescribeCommand(_BaseCommand):

class Meta:
key = "prescribe"
originate_required_fields = (
"fdb_code",
"sig",
"type_to_dispense",
"refills",
"substitutions",
"prescriber_id",
)

class Substitutions(Enum):
ALLOWED = "allowed"
NOT_ALLOWED = "not_allowed"

fdb_code: str = Field(json_schema_extra={"commands_api_name": "prescribe"})
fdb_code: str | None = Field(default=None, json_schema_extra={"commands_api_name": "prescribe"})
icd10_codes: list[str] | None = Field(
None, json_schema_extra={"commands_api_name": "indications"}
)
sig: str
sig: str | None = None
days_supply: int | None = None
quantity_to_dispense: Decimal | float | int | None = None
type_to_dispense: str
refills: int
type_to_dispense: str | None = None
refills: int | None = None
substitutions: Substitutions = Substitutions.ALLOWED # type: ignore
pharmacy: str | None = None
prescriber_id: str = Field(json_schema_extra={"commands_api_name": "prescriber"})
prescriber_id: str | None = Field(
default=None, json_schema_extra={"commands_api_name": "prescriber"}
)
note_to_pharmacist: str | None = None

@property
Expand All @@ -38,9 +48,9 @@ def values(self) -> dict:
"icd10_codes": self.icd10_codes,
"sig": self.sig,
"days_supply": self.days_supply,
"quantity_to_dispense": str(Decimal(self.quantity_to_dispense))
if self.quantity_to_dispense
else None,
"quantity_to_dispense": (
str(Decimal(self.quantity_to_dispense)) if self.quantity_to_dispense else None
),
# "type_to_dispense": self.type_to_dispense,
"refills": self.refills,
"substitutions": self.substitutions.value,
Expand Down
8 changes: 6 additions & 2 deletions canvas_sdk/commands/commands/questionnaire.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
from canvas_sdk.commands.base import _BaseCommand
from pydantic import Field

from canvas_sdk.commands.base import _BaseCommand


class QuestionnaireCommand(_BaseCommand):
"""A class for managing a Questionnaire command within a specific note."""

class Meta:
key = "questionnaire"
originate_required_fields = ("questionnaire_id",)

questionnaire_id: str = Field(json_schema_extra={"commands_api_name": "questionnaire"})
questionnaire_id: str | None = Field(
default=None, json_schema_extra={"commands_api_name": "questionnaire"}
)
result: str | None = None

@property
Expand Down
19 changes: 13 additions & 6 deletions canvas_sdk/commands/commands/reason_for_visit.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from pydantic import model_validator
from typing_extensions import Self
from typing import Literal

from pydantic_core import InitErrorDetails

from canvas_sdk.commands.base import _BaseCommand
from canvas_sdk.commands.constants import Coding
Expand All @@ -16,11 +17,17 @@ class Meta:
coding: Coding | None = None
comment: str | None = None

@model_validator(mode="after")
def _verify_structured_has_a_coding(self) -> Self:
def _get_error_details(
self, method: Literal["originate", "edit", "delete", "commit", "enter_in_error"]
) -> list[InitErrorDetails]:
errors = super()._get_error_details(method)
if self.structured and not self.coding:
raise ValueError("Structured RFV should have a coding.")
return self
errors.append(
self._create_error_detail(
"value", f"Structured RFV should have a coding.", self.coding
)
)
return errors

@classmethod
def command_schema(cls) -> dict:
Expand Down
8 changes: 6 additions & 2 deletions canvas_sdk/commands/commands/stop_medication.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
from canvas_sdk.commands.base import _BaseCommand
from pydantic import Field

from canvas_sdk.commands.base import _BaseCommand


class StopMedicationCommand(_BaseCommand):
"""A class for managing a StopMedication command within a specific note."""

class Meta:
key = "stopMedication"
originate_required_fields = ("medication_id",)

# how do we make sure this is a valid medication_id for the patient?
medication_id: str = Field(json_schema_extra={"commands_api_name": "medication"})
medication_id: str | None = Field(
default=None, json_schema_extra={"commands_api_name": "medication"}
)
rationale: str | None = None

@property
Expand Down
5 changes: 4 additions & 1 deletion canvas_sdk/commands/commands/update_goal.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class UpdateGoalCommand(_BaseCommand):

class Meta:
key = "updateGoal"
originate_required_fields = ("goal_id",)

class AchievementStatus(Enum):
IN_PROGRESS = "in-progress"
Expand All @@ -28,7 +29,9 @@ class Priority(Enum):
MEDIUM = "medium-priority"
LOW = "low-priority"

goal_id: str = Field(json_schema_extra={"commands_api_name": "goal_statement"})
goal_id: str | None = Field(
default=None, json_schema_extra={"commands_api_name": "goal_statement"}
)
due_date: datetime | None = None
achievement_status: AchievementStatus | None = None
priority: Priority | None = None
Expand Down

0 comments on commit b7315c8

Please sign in to comment.