canvas 0.1.3__py3-none-any.whl → 0.1.5__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 (70) hide show
  1. canvas-0.1.5.dist-info/METADATA +176 -0
  2. canvas-0.1.5.dist-info/RECORD +66 -0
  3. {canvas-0.1.3.dist-info → canvas-0.1.5.dist-info}/WHEEL +1 -1
  4. canvas-0.1.5.dist-info/entry_points.txt +3 -0
  5. canvas_cli/apps/__init__.py +0 -0
  6. canvas_cli/apps/auth/__init__.py +3 -0
  7. canvas_cli/apps/auth/tests.py +142 -0
  8. canvas_cli/apps/auth/utils.py +163 -0
  9. canvas_cli/apps/logs/__init__.py +3 -0
  10. canvas_cli/apps/logs/logs.py +59 -0
  11. canvas_cli/apps/plugin/__init__.py +9 -0
  12. canvas_cli/apps/plugin/plugin.py +286 -0
  13. canvas_cli/apps/plugin/tests.py +32 -0
  14. canvas_cli/conftest.py +28 -0
  15. canvas_cli/main.py +78 -0
  16. canvas_cli/templates/plugins/default/cookiecutter.json +4 -0
  17. canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/CANVAS_MANIFEST.json +29 -0
  18. canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/README.md +12 -0
  19. canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/protocols/__init__.py +0 -0
  20. canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/protocols/my_protocol.py +55 -0
  21. canvas_cli/tests.py +11 -0
  22. canvas_cli/utils/__init__.py +0 -0
  23. canvas_cli/utils/context/__init__.py +3 -0
  24. canvas_cli/utils/context/context.py +172 -0
  25. canvas_cli/utils/context/tests.py +130 -0
  26. canvas_cli/utils/print/__init__.py +3 -0
  27. canvas_cli/utils/print/print.py +60 -0
  28. canvas_cli/utils/print/tests.py +70 -0
  29. canvas_cli/utils/urls/__init__.py +3 -0
  30. canvas_cli/utils/urls/tests.py +12 -0
  31. canvas_cli/utils/urls/urls.py +27 -0
  32. canvas_cli/utils/validators/__init__.py +3 -0
  33. canvas_cli/utils/validators/manifest_schema.py +80 -0
  34. canvas_cli/utils/validators/tests.py +36 -0
  35. canvas_cli/utils/validators/validators.py +40 -0
  36. canvas_sdk/__init__.py +0 -0
  37. canvas_sdk/commands/__init__.py +27 -0
  38. canvas_sdk/commands/base.py +118 -0
  39. canvas_sdk/commands/commands/assess.py +48 -0
  40. canvas_sdk/commands/commands/diagnose.py +44 -0
  41. canvas_sdk/commands/commands/goal.py +48 -0
  42. canvas_sdk/commands/commands/history_present_illness.py +15 -0
  43. canvas_sdk/commands/commands/medication_statement.py +28 -0
  44. canvas_sdk/commands/commands/plan.py +15 -0
  45. canvas_sdk/commands/commands/prescribe.py +48 -0
  46. canvas_sdk/commands/commands/questionnaire.py +17 -0
  47. canvas_sdk/commands/commands/reason_for_visit.py +36 -0
  48. canvas_sdk/commands/commands/stop_medication.py +18 -0
  49. canvas_sdk/commands/commands/update_goal.py +48 -0
  50. canvas_sdk/commands/constants.py +9 -0
  51. canvas_sdk/commands/tests/test_utils.py +195 -0
  52. canvas_sdk/commands/tests/tests.py +407 -0
  53. canvas_sdk/data/__init__.py +0 -0
  54. canvas_sdk/effects/__init__.py +1 -0
  55. canvas_sdk/effects/banner_alert/banner_alert.py +37 -0
  56. canvas_sdk/effects/banner_alert/constants.py +19 -0
  57. canvas_sdk/effects/base.py +30 -0
  58. canvas_sdk/events/__init__.py +1 -0
  59. canvas_sdk/protocols/__init__.py +1 -0
  60. canvas_sdk/protocols/base.py +12 -0
  61. canvas_sdk/tests/__init__.py +0 -0
  62. canvas_sdk/utils/__init__.py +3 -0
  63. canvas_sdk/utils/http.py +72 -0
  64. canvas_sdk/utils/tests.py +63 -0
  65. canvas_sdk/views/__init__.py +0 -0
  66. canvas/main.py +0 -19
  67. canvas-0.1.3.dist-info/METADATA +0 -285
  68. canvas-0.1.3.dist-info/RECORD +0 -6
  69. canvas-0.1.3.dist-info/entry_points.txt +0 -3
  70. {canvas → canvas_cli}/__init__.py +0 -0
@@ -0,0 +1,118 @@
1
+ import json
2
+ from enum import EnumType
3
+ from typing import get_args
4
+
5
+ from pydantic import BaseModel, ConfigDict, model_validator
6
+ from typing_extensions import Self
7
+
8
+ from canvas_sdk.effects import Effect
9
+
10
+
11
+ class _BaseCommand(BaseModel):
12
+ model_config = ConfigDict(strict=True, validate_assignment=True)
13
+
14
+ class Meta:
15
+ key = ""
16
+
17
+ # todo: update int to str as we should use external identifiers
18
+ note_id: int | None = None
19
+ command_uuid: str | None = None
20
+ user_id: int
21
+
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
27
+
28
+ @property
29
+ def values(self) -> dict:
30
+ return {}
31
+
32
+ @classmethod
33
+ def _get_property_choices(cls, name: str, schema: dict) -> list[dict] | None:
34
+ definition = schema.get("properties", {}).get(name, {})
35
+ if not (choice_ref := next((a.get("$ref") for a in definition.get("anyOf", [])), None)):
36
+ return None
37
+ choice_key = choice_ref.split("#/$defs/")[-1]
38
+ return schema.get("$defs", {}).get(choice_key, {}).get("enum")
39
+
40
+ @classmethod
41
+ def _get_property_type(cls, name: str) -> type:
42
+ annotation = cls.model_fields[name].annotation
43
+ if annotation_args := get_args(annotation):
44
+ # if its a union, take the first one (which is not None)
45
+ annotation = annotation_args[0]
46
+
47
+ if type(annotation) is EnumType:
48
+ return str
49
+
50
+ return annotation
51
+
52
+ @classmethod
53
+ def command_schema(cls) -> dict:
54
+ """The schema of the command."""
55
+ base_properties = {"note_id", "command_uuid", "user_id"}
56
+ schema = cls.model_json_schema()
57
+ return {
58
+ definition.get("commands_api_name", name): {
59
+ "required": name in schema["required"],
60
+ "type": cls._get_property_type(name),
61
+ "choices": cls._get_property_choices(name, schema),
62
+ }
63
+ for name, definition in schema["properties"].items()
64
+ if name not in base_properties
65
+ }
66
+
67
+ def originate(self) -> Effect:
68
+ """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
+ }
79
+
80
+ def edit(self) -> Effect:
81
+ """Edit the command."""
82
+ if not self.command_uuid:
83
+ raise AttributeError("Command uuid is required to edit a command")
84
+ return {
85
+ "type": f"EDIT_{self.Meta.key.upper()}_COMMAND",
86
+ "payload": {
87
+ "user": self.user_id,
88
+ "command": self.command_uuid,
89
+ "data": self.values,
90
+ },
91
+ }
92
+
93
+ def delete(self) -> Effect:
94
+ """Delete the command."""
95
+ if not self.command_uuid:
96
+ raise AttributeError("Command uuid is required to delete a command")
97
+ return {
98
+ "type": f"DELETE_{self.Meta.key.upper()}_COMMAND",
99
+ "payload": {"command": self.command_uuid, "user": self.user_id},
100
+ }
101
+
102
+ def commit(self) -> Effect:
103
+ """Commit the command."""
104
+ if not self.command_uuid:
105
+ raise AttributeError("Command uuid is required to commit a command")
106
+ return {
107
+ "type": f"COMMIT_{self.Meta.key.upper()}_COMMAND",
108
+ "payload": {"command": self.command_uuid, "user": self.user_id},
109
+ }
110
+
111
+ def enter_in_error(self) -> Effect:
112
+ """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")
115
+ return {
116
+ "type": f"ENTER_IN_ERROR_{self.Meta.key.upper()}_COMMAND",
117
+ "payload": {"command": self.command_uuid, "user": self.user_id},
118
+ }
@@ -0,0 +1,48 @@
1
+ from enum import Enum
2
+
3
+ from canvas_sdk.commands.base import _BaseCommand
4
+ from pydantic import Field
5
+
6
+
7
+ class AssessCommand(_BaseCommand):
8
+ """A class for managing an Assess command within a specific note."""
9
+
10
+ class Meta:
11
+ key = "assess"
12
+
13
+ class Status(Enum):
14
+ IMPROVED = "improved"
15
+ STABLE = "stable"
16
+ DETERIORATED = "deteriorated"
17
+
18
+ condition_id: str = Field(json_schema_extra={"commands_api_name": "condition"})
19
+ background: str | None = None
20
+ status: Status | None = None
21
+ narrative: str | None = None
22
+
23
+ @property
24
+ def values(self) -> dict:
25
+ """The Assess command's field values."""
26
+ return {
27
+ "condition_id": self.condition_id,
28
+ "background": self.background,
29
+ "status": self.status.value if self.status else None,
30
+ "narrative": self.narrative,
31
+ }
32
+
33
+
34
+ # how do we make sure that condition_id is a valid condition for the patient?
35
+
36
+ # idea1:
37
+ # create a class attribute 'pre_validate_condition': bool
38
+ # True: before doing any actions against the home-app instance, will first check
39
+ # if its an active condition for that patient, and it not then logs a message
40
+ # and doesn't do the action. can even create a cache so that we dont always
41
+ # have to keep asking home-app for every patient
42
+ # False: still tries to do it but will run up against whatever home-app barriers
43
+ # we have against actions like that (if those dont already exist, then
44
+ # definitely create them!)
45
+
46
+
47
+ # idea2:
48
+ # validator that checks that condition.patient is the same as note.patient
@@ -0,0 +1,44 @@
1
+ from datetime import datetime
2
+
3
+ from pydantic import Field
4
+
5
+ from canvas_sdk.commands.base import _BaseCommand
6
+
7
+
8
+ class DiagnoseCommand(_BaseCommand):
9
+ """A class for managing a Diagnose command within a specific note."""
10
+
11
+ class Meta:
12
+ key = "diagnose"
13
+
14
+ icd10_code: str = Field(json_schema_extra={"commands_api_name": "diagnose"})
15
+ background: str | None = None
16
+ approximate_date_of_onset: datetime | None = None
17
+ today_assessment: str | None = None
18
+
19
+ @property
20
+ def values(self) -> dict:
21
+ """The Diagnose command's field values."""
22
+ return {
23
+ "icd10_code": self.icd10_code,
24
+ "background": self.background,
25
+ "approximate_date_of_onset": (
26
+ self.approximate_date_of_onset.isoformat()
27
+ if self.approximate_date_of_onset
28
+ else None
29
+ ),
30
+ "today_assessment": self.today_assessment,
31
+ }
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
@@ -0,0 +1,48 @@
1
+ from datetime import datetime
2
+ from enum import Enum
3
+
4
+ from canvas_sdk.commands.base import _BaseCommand
5
+
6
+
7
+ class GoalCommand(_BaseCommand):
8
+ """A class for managing a Goal command within a specific note."""
9
+
10
+ class Meta:
11
+ key = "goal"
12
+
13
+ class Priority(Enum):
14
+ HIGH = "high-priority"
15
+ MEDIUM = "medium-priority"
16
+ LOW = "low-priority"
17
+
18
+ class AchievementStatus(Enum):
19
+ IN_PROGRESS = "in-progress"
20
+ IMPROVING = "improving"
21
+ WORSENING = "worsening"
22
+ NO_CHANGE = "no-change"
23
+ ACHIEVED = "achieved"
24
+ SUSTAINING = "sustaining"
25
+ NOT_ACHIEVED = "not-achieved"
26
+ NO_PROGRESS = "no-progress"
27
+ NOT_ATTAINABLE = "not-attainable"
28
+
29
+ goal_statement: str
30
+ start_date: datetime | None = None
31
+ due_date: datetime | None = None
32
+ achievement_status: AchievementStatus | None = None
33
+ priority: Priority | None = None
34
+ progress: str | None = None
35
+
36
+ @property
37
+ def values(self) -> dict:
38
+ """The Goal command's field values."""
39
+ return {
40
+ "goal_statement": self.goal_statement,
41
+ "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
+ "achievement_status": (
44
+ self.achievement_status.value if self.achievement_status else None
45
+ ),
46
+ "priority": (self.priority.value if self.priority else None),
47
+ "progress": self.progress,
48
+ }
@@ -0,0 +1,15 @@
1
+ from canvas_sdk.commands.base import _BaseCommand
2
+
3
+
4
+ class HistoryOfPresentIllnessCommand(_BaseCommand):
5
+ """A class for managing a HPI command within a specific note."""
6
+
7
+ class Meta:
8
+ key = "hpi"
9
+
10
+ narrative: str
11
+
12
+ @property
13
+ def values(self) -> dict:
14
+ """The HPI command's field values."""
15
+ return {"narrative": self.narrative}
@@ -0,0 +1,28 @@
1
+ from canvas_sdk.commands.base import _BaseCommand
2
+ from pydantic import Field
3
+
4
+
5
+ class MedicationStatementCommand(_BaseCommand):
6
+ """A class for managing a MedicationStatement command within a specific note."""
7
+
8
+ class Meta:
9
+ key = "medicationStatement"
10
+
11
+ fdb_code: str = Field(json_schema_extra={"commands_api_name": "medication"})
12
+ sig: str | None = None
13
+
14
+ @property
15
+ def values(self) -> dict:
16
+ """The MedicationStatement command's field values."""
17
+ return {"fdb_code": self.fdb_code, "sig": self.sig}
18
+
19
+
20
+ # how do we make sure fdb_code is a valid code?
21
+
22
+ # idea1:
23
+ # create an auto-generated enum class of all possible fdbs, then type the field as that enum
24
+ # will require releasing a new version with the new codes every year, and devs will need to update
25
+ # to make sure they have the latest version to get the right set of codes.
26
+
27
+ # idea2:
28
+ # see if we can get ValueSets to play nicely with pydantic
@@ -0,0 +1,15 @@
1
+ from canvas_sdk.commands.base import _BaseCommand
2
+
3
+
4
+ class PlanCommand(_BaseCommand):
5
+ """A class for managing a Plan command within a specific note."""
6
+
7
+ class Meta:
8
+ key = "plan"
9
+
10
+ narrative: str
11
+
12
+ @property
13
+ def values(self) -> dict:
14
+ """The Plan command's field values."""
15
+ return {"narrative": self.narrative}
@@ -0,0 +1,48 @@
1
+ from decimal import Decimal
2
+ from enum import Enum
3
+
4
+ from pydantic import Field
5
+
6
+ from canvas_sdk.commands.base import _BaseCommand
7
+
8
+
9
+ class PrescribeCommand(_BaseCommand):
10
+ """A class for managing a Prescribe command within a specific note."""
11
+
12
+ class Meta:
13
+ key = "prescribe"
14
+
15
+ class Substitutions(Enum):
16
+ ALLOWED = "allowed"
17
+ NOT_ALLOWED = "not_allowed"
18
+
19
+ fdb_code: str = Field(json_schema_extra={"commands_api_name": "prescribe"})
20
+ icd10_codes: list[str] | None = Field(
21
+ None, json_schema_extra={"commandsd_api_name": "indications"}
22
+ )
23
+ sig: str
24
+ 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
29
+ pharmacy: str | None = None
30
+ prescriber_id: str = Field(json_schema_extra={"commands_api_name": "prescriber"})
31
+ note_to_pharmacist: str | None = None
32
+
33
+ @property
34
+ def values(self) -> dict:
35
+ """The Prescribe command's field values."""
36
+ return {
37
+ "fdb_code": self.fdb_code,
38
+ "icd10_codes": self.icd10_codes,
39
+ "sig": self.sig,
40
+ "days_supply": self.days_supply,
41
+ "quantity_to_dispense": self.quantity_to_dispense,
42
+ "type_to_dispense": self.type_to_dispense,
43
+ "refills": self.refills,
44
+ "substitutions": self.substitutions,
45
+ "pharmacy": self.pharmacy,
46
+ "prescriber_id": self.prescriber_id,
47
+ "note_to_pharmacist": self.note_to_pharmacist,
48
+ }
@@ -0,0 +1,17 @@
1
+ from canvas_sdk.commands.base import _BaseCommand
2
+ from pydantic import Field
3
+
4
+
5
+ class QuestionnaireCommand(_BaseCommand):
6
+ """A class for managing a Questionnaire command within a specific note."""
7
+
8
+ class Meta:
9
+ key = "questionnaire"
10
+
11
+ questionnaire_id: str = Field(json_schema_extra={"commands_api_name": "questionnaire"})
12
+ result: str | None = None
13
+
14
+ @property
15
+ def values(self) -> dict:
16
+ """The Questionnaire command's field values."""
17
+ return {"questionnaire_id": self.questionnaire_id, "result": self.result}
@@ -0,0 +1,36 @@
1
+ from pydantic import model_validator
2
+ from typing_extensions import Self
3
+
4
+ from canvas_sdk.commands.base import _BaseCommand
5
+ from canvas_sdk.commands.constants import Coding
6
+
7
+
8
+ class ReasonForVisitCommand(_BaseCommand):
9
+ """A class for managing a ReasonForVisit command within a specific note."""
10
+
11
+ class Meta:
12
+ key = "reasonForVisit"
13
+
14
+ structured: bool = False
15
+ # how do we make sure that coding is a valid rfv coding from their home-app?
16
+ coding: Coding | None = None
17
+ comment: str | None = None
18
+
19
+ @model_validator(mode="after")
20
+ def _verify_structured_has_a_coding(self) -> Self:
21
+ if self.structured and not self.coding:
22
+ raise ValueError("Structured RFV should have a coding.")
23
+ return self
24
+
25
+ @classmethod
26
+ def command_schema(cls) -> dict:
27
+ """The schema of the command."""
28
+ command_schema = super().command_schema()
29
+ # the commands api does not include the 'structured' field in the fields response
30
+ command_schema.pop("structured")
31
+ return command_schema
32
+
33
+ @property
34
+ def values(self) -> dict:
35
+ """The ReasonForVisit command's field values."""
36
+ return {"structured": self.structured, "coding": self.coding, "comment": self.comment}
@@ -0,0 +1,18 @@
1
+ from canvas_sdk.commands.base import _BaseCommand
2
+ from pydantic import Field
3
+
4
+
5
+ class StopMedicationCommand(_BaseCommand):
6
+ """A class for managing a StopMedication command within a specific note."""
7
+
8
+ class Meta:
9
+ key = "stopMedication"
10
+
11
+ # 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"})
13
+ rationale: str | None = None
14
+
15
+ @property
16
+ def values(self) -> dict:
17
+ """The StopMedication command's field values."""
18
+ return {"medication_id": self.medication_id, "rationale": self.rationale}
@@ -0,0 +1,48 @@
1
+ from datetime import datetime
2
+ from enum import Enum
3
+
4
+ from pydantic import Field
5
+
6
+ from canvas_sdk.commands.base import _BaseCommand
7
+
8
+
9
+ class UpdateGoalCommand(_BaseCommand):
10
+ """A class for managing an UpdateGoal command within a specific note."""
11
+
12
+ class Meta:
13
+ key = "updateGoal"
14
+
15
+ class AchievementStatus(Enum):
16
+ IN_PROGRESS = "in-progress"
17
+ IMPROVING = "improving"
18
+ WORSENING = "worsening"
19
+ NO_CHANGE = "no-change"
20
+ ACHIEVED = "achieved"
21
+ SUSTAINING = "sustaining"
22
+ NOT_ACHIEVED = "not-achieved"
23
+ NO_PROGRESS = "no-progress"
24
+ NOT_ATTAINABLE = "not-attainable"
25
+
26
+ class Priority(Enum):
27
+ HIGH = "high-priority"
28
+ MEDIUM = "medium-priority"
29
+ LOW = "low-priority"
30
+
31
+ goal_id: str = Field(json_schema_extra={"commands_api_name": "goal_statement"})
32
+ due_date: datetime | None = None
33
+ achievement_status: AchievementStatus | None = None
34
+ priority: Priority | None = None
35
+ progress: str | None = None
36
+
37
+ @property
38
+ def values(self) -> dict:
39
+ """The UpdateGoal command's field values."""
40
+ return {
41
+ "goal_id": self.goal_id,
42
+ "due_date": (self.due_date.isoformat() if self.due_date else None),
43
+ "achievement_status": (
44
+ self.achievement_status.value if self.achievement_status else None
45
+ ),
46
+ "priority": (self.priority.value if self.priority else None),
47
+ "progress": self.progress,
48
+ }
@@ -0,0 +1,9 @@
1
+ from typing_extensions import NotRequired, TypedDict
2
+
3
+
4
+ class Coding(TypedDict):
5
+ """Coding object in Canvas."""
6
+
7
+ system: str
8
+ code: str
9
+ display: NotRequired[str]