canvas 0.63.0__py3-none-any.whl → 0.89.0__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.
Files changed (185) hide show
  1. {canvas-0.63.0.dist-info → canvas-0.89.0.dist-info}/METADATA +4 -1
  2. {canvas-0.63.0.dist-info → canvas-0.89.0.dist-info}/RECORD +184 -98
  3. {canvas-0.63.0.dist-info → canvas-0.89.0.dist-info}/WHEEL +1 -1
  4. canvas_cli/apps/emit/event_fixtures/UNKNOWN.ndjson +1 -0
  5. canvas_cli/apps/logs/logs.py +386 -22
  6. canvas_cli/main.py +3 -1
  7. canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/tests/test_models.py +46 -4
  8. canvas_cli/utils/context/context.py +13 -13
  9. canvas_cli/utils/validators/manifest_schema.py +26 -1
  10. canvas_generated/messages/effects_pb2.py +5 -5
  11. canvas_generated/messages/effects_pb2.pyi +108 -2
  12. canvas_generated/messages/events_pb2.py +6 -6
  13. canvas_generated/messages/events_pb2.pyi +282 -2
  14. canvas_sdk/clients/__init__.py +1 -0
  15. canvas_sdk/clients/llms/__init__.py +17 -0
  16. canvas_sdk/clients/llms/libraries/__init__.py +11 -0
  17. canvas_sdk/clients/llms/libraries/llm_anthropic.py +87 -0
  18. canvas_sdk/clients/llms/libraries/llm_api.py +143 -0
  19. canvas_sdk/clients/llms/libraries/llm_google.py +92 -0
  20. canvas_sdk/clients/llms/libraries/llm_openai.py +98 -0
  21. canvas_sdk/clients/llms/structures/__init__.py +9 -0
  22. canvas_sdk/clients/llms/structures/llm_response.py +33 -0
  23. canvas_sdk/clients/llms/structures/llm_tokens.py +53 -0
  24. canvas_sdk/clients/llms/structures/llm_turn.py +47 -0
  25. canvas_sdk/clients/llms/structures/settings/__init__.py +13 -0
  26. canvas_sdk/clients/llms/structures/settings/llm_settings.py +27 -0
  27. canvas_sdk/clients/llms/structures/settings/llm_settings_anthropic.py +43 -0
  28. canvas_sdk/clients/llms/structures/settings/llm_settings_gemini.py +40 -0
  29. canvas_sdk/clients/llms/structures/settings/llm_settings_gpt4.py +40 -0
  30. canvas_sdk/clients/llms/structures/settings/llm_settings_gpt5.py +48 -0
  31. canvas_sdk/clients/third_party.py +3 -0
  32. canvas_sdk/commands/__init__.py +12 -0
  33. canvas_sdk/commands/base.py +33 -2
  34. canvas_sdk/commands/commands/adjust_prescription.py +4 -0
  35. canvas_sdk/commands/commands/custom_command.py +86 -0
  36. canvas_sdk/commands/commands/family_history.py +17 -1
  37. canvas_sdk/commands/commands/immunization_statement.py +42 -2
  38. canvas_sdk/commands/commands/medication_statement.py +16 -1
  39. canvas_sdk/commands/commands/past_surgical_history.py +16 -1
  40. canvas_sdk/commands/commands/perform.py +18 -1
  41. canvas_sdk/commands/commands/prescribe.py +8 -9
  42. canvas_sdk/commands/commands/refill.py +5 -5
  43. canvas_sdk/commands/commands/resolve_condition.py +5 -5
  44. canvas_sdk/commands/commands/review/__init__.py +3 -0
  45. canvas_sdk/commands/commands/review/base.py +72 -0
  46. canvas_sdk/commands/commands/review/imaging.py +13 -0
  47. canvas_sdk/commands/commands/review/lab.py +13 -0
  48. canvas_sdk/commands/commands/review/referral.py +13 -0
  49. canvas_sdk/commands/commands/review/uncategorized_document.py +13 -0
  50. canvas_sdk/commands/validation.py +43 -0
  51. canvas_sdk/effects/batch_originate.py +22 -0
  52. canvas_sdk/effects/calendar/__init__.py +13 -3
  53. canvas_sdk/effects/calendar/{create_calendar.py → calendar.py} +19 -5
  54. canvas_sdk/effects/calendar/event.py +172 -0
  55. canvas_sdk/effects/claim_label.py +93 -0
  56. canvas_sdk/effects/claim_line_item.py +47 -0
  57. canvas_sdk/effects/claim_queue.py +49 -0
  58. canvas_sdk/effects/fax/__init__.py +3 -0
  59. canvas_sdk/effects/fax/base.py +77 -0
  60. canvas_sdk/effects/fax/note.py +42 -0
  61. canvas_sdk/effects/metadata.py +15 -1
  62. canvas_sdk/effects/note/__init__.py +8 -1
  63. canvas_sdk/effects/note/appointment.py +135 -7
  64. canvas_sdk/effects/note/base.py +17 -0
  65. canvas_sdk/effects/note/message.py +22 -14
  66. canvas_sdk/effects/note/note.py +150 -1
  67. canvas_sdk/effects/observation/__init__.py +11 -0
  68. canvas_sdk/effects/observation/base.py +206 -0
  69. canvas_sdk/effects/patient/__init__.py +2 -0
  70. canvas_sdk/effects/patient/base.py +8 -0
  71. canvas_sdk/effects/payment/__init__.py +11 -0
  72. canvas_sdk/effects/payment/base.py +355 -0
  73. canvas_sdk/effects/payment/post_claim_payment.py +49 -0
  74. canvas_sdk/effects/send_contact_verification.py +42 -0
  75. canvas_sdk/effects/task/__init__.py +2 -1
  76. canvas_sdk/effects/task/task.py +30 -0
  77. canvas_sdk/effects/validation/__init__.py +3 -0
  78. canvas_sdk/effects/validation/base.py +92 -0
  79. canvas_sdk/events/base.py +15 -0
  80. canvas_sdk/handlers/application.py +7 -7
  81. canvas_sdk/handlers/simple_api/api.py +1 -4
  82. canvas_sdk/handlers/simple_api/websocket.py +1 -4
  83. canvas_sdk/handlers/utils.py +14 -0
  84. canvas_sdk/questionnaires/utils.py +1 -0
  85. canvas_sdk/templates/utils.py +17 -4
  86. canvas_sdk/test_utils/factories/FACTORY_GUIDE.md +362 -0
  87. canvas_sdk/test_utils/factories/__init__.py +115 -0
  88. canvas_sdk/test_utils/factories/calendar.py +24 -0
  89. canvas_sdk/test_utils/factories/claim.py +81 -0
  90. canvas_sdk/test_utils/factories/claim_diagnosis_code.py +16 -0
  91. canvas_sdk/test_utils/factories/coverage.py +17 -0
  92. canvas_sdk/test_utils/factories/imaging.py +74 -0
  93. canvas_sdk/test_utils/factories/lab.py +192 -0
  94. canvas_sdk/test_utils/factories/medication_history.py +75 -0
  95. canvas_sdk/test_utils/factories/note.py +52 -0
  96. canvas_sdk/test_utils/factories/organization.py +50 -0
  97. canvas_sdk/test_utils/factories/practicelocation.py +88 -0
  98. canvas_sdk/test_utils/factories/referral.py +81 -0
  99. canvas_sdk/test_utils/factories/staff.py +111 -0
  100. canvas_sdk/test_utils/factories/task.py +66 -0
  101. canvas_sdk/test_utils/factories/uncategorized_clinical_document.py +48 -0
  102. canvas_sdk/utils/metrics.py +4 -1
  103. canvas_sdk/v1/data/__init__.py +66 -7
  104. canvas_sdk/v1/data/allergy_intolerance.py +5 -11
  105. canvas_sdk/v1/data/appointment.py +18 -4
  106. canvas_sdk/v1/data/assessment.py +2 -12
  107. canvas_sdk/v1/data/banner_alert.py +2 -4
  108. canvas_sdk/v1/data/base.py +53 -14
  109. canvas_sdk/v1/data/billing.py +8 -11
  110. canvas_sdk/v1/data/calendar.py +64 -0
  111. canvas_sdk/v1/data/care_team.py +4 -10
  112. canvas_sdk/v1/data/claim.py +172 -66
  113. canvas_sdk/v1/data/claim_diagnosis_code.py +19 -0
  114. canvas_sdk/v1/data/claim_line_item.py +2 -5
  115. canvas_sdk/v1/data/coding.py +19 -0
  116. canvas_sdk/v1/data/command.py +2 -4
  117. canvas_sdk/v1/data/common.py +10 -0
  118. canvas_sdk/v1/data/compound_medication.py +3 -4
  119. canvas_sdk/v1/data/condition.py +4 -9
  120. canvas_sdk/v1/data/coverage.py +66 -26
  121. canvas_sdk/v1/data/detected_issue.py +20 -20
  122. canvas_sdk/v1/data/device.py +2 -14
  123. canvas_sdk/v1/data/discount.py +2 -5
  124. canvas_sdk/v1/data/encounter.py +44 -0
  125. canvas_sdk/v1/data/facility.py +1 -0
  126. canvas_sdk/v1/data/goal.py +2 -14
  127. canvas_sdk/v1/data/imaging.py +4 -30
  128. canvas_sdk/v1/data/immunization.py +7 -15
  129. canvas_sdk/v1/data/lab.py +12 -65
  130. canvas_sdk/v1/data/line_item_transaction.py +2 -5
  131. canvas_sdk/v1/data/medication.py +3 -8
  132. canvas_sdk/v1/data/medication_history.py +142 -0
  133. canvas_sdk/v1/data/medication_statement.py +41 -0
  134. canvas_sdk/v1/data/message.py +4 -8
  135. canvas_sdk/v1/data/note.py +37 -38
  136. canvas_sdk/v1/data/observation.py +9 -36
  137. canvas_sdk/v1/data/organization.py +70 -9
  138. canvas_sdk/v1/data/patient.py +8 -12
  139. canvas_sdk/v1/data/patient_consent.py +4 -14
  140. canvas_sdk/v1/data/payment_collection.py +2 -5
  141. canvas_sdk/v1/data/posting.py +3 -9
  142. canvas_sdk/v1/data/practicelocation.py +66 -7
  143. canvas_sdk/v1/data/protocol_override.py +3 -4
  144. canvas_sdk/v1/data/protocol_result.py +3 -3
  145. canvas_sdk/v1/data/questionnaire.py +10 -26
  146. canvas_sdk/v1/data/reason_for_visit.py +2 -6
  147. canvas_sdk/v1/data/referral.py +41 -17
  148. canvas_sdk/v1/data/staff.py +34 -26
  149. canvas_sdk/v1/data/stop_medication_event.py +27 -0
  150. canvas_sdk/v1/data/task.py +30 -11
  151. canvas_sdk/v1/data/team.py +2 -4
  152. canvas_sdk/v1/data/uncategorized_clinical_document.py +84 -0
  153. canvas_sdk/v1/data/user.py +14 -0
  154. canvas_sdk/v1/data/utils.py +5 -0
  155. canvas_sdk/value_set/v2026/__init__.py +1 -0
  156. canvas_sdk/value_set/v2026/adverse_event.py +157 -0
  157. canvas_sdk/value_set/v2026/allergy.py +116 -0
  158. canvas_sdk/value_set/v2026/assessment.py +466 -0
  159. canvas_sdk/value_set/v2026/communication.py +496 -0
  160. canvas_sdk/value_set/v2026/condition.py +52934 -0
  161. canvas_sdk/value_set/v2026/device.py +315 -0
  162. canvas_sdk/value_set/v2026/diagnostic_study.py +5243 -0
  163. canvas_sdk/value_set/v2026/encounter.py +2714 -0
  164. canvas_sdk/value_set/v2026/immunization.py +297 -0
  165. canvas_sdk/value_set/v2026/individual_characteristic.py +339 -0
  166. canvas_sdk/value_set/v2026/intervention.py +1703 -0
  167. canvas_sdk/value_set/v2026/laboratory_test.py +1831 -0
  168. canvas_sdk/value_set/v2026/medication.py +8218 -0
  169. canvas_sdk/value_set/v2026/no_qdm_category_assigned.py +26493 -0
  170. canvas_sdk/value_set/v2026/physical_exam.py +342 -0
  171. canvas_sdk/value_set/v2026/procedure.py +27869 -0
  172. canvas_sdk/value_set/v2026/symptom.py +625 -0
  173. logger/logger.py +30 -31
  174. logger/logstash.py +282 -0
  175. logger/pubsub.py +26 -0
  176. plugin_runner/allowed-module-imports.json +940 -9
  177. plugin_runner/generate_allowed_imports.py +1 -0
  178. plugin_runner/installation.py +2 -2
  179. plugin_runner/plugin_runner.py +21 -24
  180. plugin_runner/sandbox.py +34 -0
  181. protobufs/canvas_generated/messages/effects.proto +65 -0
  182. protobufs/canvas_generated/messages/events.proto +150 -51
  183. settings.py +27 -11
  184. canvas_sdk/effects/calendar/create_event.py +0 -43
  185. {canvas-0.63.0.dist-info → canvas-0.89.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,42 @@
1
+ from typing import Any
2
+ from uuid import UUID
3
+
4
+ from pydantic_core import InitErrorDetails
5
+
6
+ from canvas_sdk.effects.base import EffectType, _BaseEffect
7
+ from canvas_sdk.v1.data import PatientContactPoint
8
+
9
+
10
+ class SendContactVerificationEffect(_BaseEffect):
11
+ """
12
+ An Effect that will send a verification for a Patient Contact Point.
13
+ """
14
+
15
+ class Meta:
16
+ effect_type = EffectType.PATIENT_PORTAL__SEND_CONTACT_VERIFICATION
17
+
18
+ contact_point_id: str | UUID
19
+
20
+ @property
21
+ def values(self) -> dict[str, Any]:
22
+ """The contact point id."""
23
+ return {"contact_point_id": self.contact_point_id}
24
+
25
+ def _get_error_details(self, method: Any) -> list[InitErrorDetails]:
26
+ errors = super()._get_error_details(method)
27
+
28
+ contact_point_exists = PatientContactPoint.objects.filter(id=self.contact_point_id).exists()
29
+
30
+ if not contact_point_exists:
31
+ errors.append(
32
+ self._create_error_detail(
33
+ "value",
34
+ "Patient Contact Point does not exist",
35
+ self.contact_point_id,
36
+ )
37
+ )
38
+
39
+ return errors
40
+
41
+
42
+ __exports__ = ("SendContactVerificationEffect",)
@@ -1,8 +1,9 @@
1
- from .task import AddTask, AddTaskComment, TaskStatus, UpdateTask
1
+ from .task import AddTask, AddTaskComment, TaskMetadata, TaskStatus, UpdateTask
2
2
 
3
3
  __all__ = __exports__ = (
4
4
  "AddTask",
5
5
  "AddTaskComment",
6
6
  "TaskStatus",
7
+ "TaskMetadata",
7
8
  "UpdateTask",
8
9
  )
@@ -4,8 +4,11 @@ from typing import Any, Self, cast
4
4
  from uuid import UUID
5
5
 
6
6
  from pydantic import model_validator
7
+ from pydantic_core import InitErrorDetails
7
8
 
8
9
  from canvas_sdk.effects.base import EffectType, _BaseEffect
10
+ from canvas_sdk.effects.metadata import BaseMetadata
11
+ from canvas_sdk.v1.data import Task
9
12
 
10
13
 
11
14
  class TaskStatus(Enum):
@@ -140,9 +143,36 @@ class UpdateTask(_BaseEffect):
140
143
  return value_dict
141
144
 
142
145
 
146
+ class TaskMetadata(BaseMetadata):
147
+ """Task Metadata."""
148
+
149
+ class Meta:
150
+ effect_type = "TASK_METADATA"
151
+
152
+ task_id: str
153
+
154
+ def _get_error_details(self, method: Any) -> list[InitErrorDetails]:
155
+ """Get the error details for the effect.
156
+ If task_id is not found, return an error detail.
157
+ """
158
+ errors = super()._get_error_details(method)
159
+
160
+ if not Task.objects.filter(id=self.task_id).exists():
161
+ errors.append(
162
+ self._create_error_detail(
163
+ "task_id",
164
+ f"Task with id: {self.task_id} does not exist.",
165
+ self.task_id,
166
+ )
167
+ )
168
+
169
+ return errors
170
+
171
+
143
172
  __exports__ = (
144
173
  "AddTask",
145
174
  "AddTaskComment",
146
175
  "TaskStatus",
176
+ "TaskMetadata",
147
177
  "UpdateTask",
148
178
  )
@@ -0,0 +1,3 @@
1
+ from .base import ValidationError
2
+
3
+ __all__ = __exports__ = ("ValidationError",)
@@ -0,0 +1,92 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any, Self
3
+
4
+ from pydantic import Field
5
+
6
+ from canvas_sdk.effects import EffectType
7
+ from canvas_sdk.effects.base import _BaseEffect
8
+
9
+
10
+ @dataclass
11
+ class ValidationError:
12
+ """
13
+ Represents a validation error for an effect or command.
14
+
15
+ Attributes:
16
+ message: The validation error message to display
17
+ """
18
+
19
+ message: str
20
+
21
+ def __post_init__(self) -> None:
22
+ """
23
+ Validate and normalize the error message after initialization.
24
+
25
+ Raises:
26
+ ValueError: If message is empty
27
+ """
28
+ if not self.message or not self.message.strip():
29
+ raise ValueError("Error message cannot be empty")
30
+
31
+ self.message = self.message.strip()
32
+
33
+ def to_dict(self) -> dict[str, str]:
34
+ """Convert the validation error to a dictionary."""
35
+ return {"message": self.message}
36
+
37
+ def __repr__(self) -> str:
38
+ return f"ValidationError(message={self.message!r})"
39
+
40
+
41
+ class _BaseValidationErrorEffect(_BaseEffect):
42
+ """
43
+ Abstract effect to abort an event.
44
+ """
45
+
46
+ class Meta:
47
+ effect_type = EffectType.UNKNOWN_EFFECT
48
+
49
+ errors: list[ValidationError] = Field(default_factory=list)
50
+
51
+ def add_error(self, message: str | ValidationError) -> Self:
52
+ """
53
+ Add a validation error to the effect.
54
+
55
+ This method allows incremental building of validation errors
56
+ and supports method chaining.
57
+
58
+ Args:
59
+ message: The error message to display, or a ValidationError object
60
+
61
+ Returns:
62
+ Self for method chaining
63
+
64
+ Raises:
65
+ ValueError: If message is empty
66
+
67
+ Example:
68
+ effect = CommandValidationErrorEffect()
69
+ effect.add_error("Narrative is required")
70
+ effect.add_error("Dosage must be positive")
71
+
72
+ # Using ValidationError objects
73
+ error = ValidationError("Already validated error")
74
+ effect.add_error(error)
75
+
76
+ # Method chaining
77
+ effect.add_error("Error 1").add_error("Error 2")
78
+ """
79
+ if isinstance(message, ValidationError):
80
+ self.errors.append(message)
81
+ else:
82
+ error = ValidationError(message=message)
83
+ self.errors.append(error)
84
+ return self
85
+
86
+ @property
87
+ def values(self) -> dict[str, Any]:
88
+ """Payload to include in the Effect."""
89
+ return {"errors": [error.to_dict() for error in self.errors]}
90
+
91
+
92
+ __exports__ = ("ValidationError",)
canvas_sdk/events/base.py CHANGED
@@ -8,6 +8,7 @@ from django.db import models
8
8
 
9
9
  from canvas_generated.messages.events_pb2 import Event as EventRequest
10
10
  from canvas_generated.messages.events_pb2 import EventType
11
+ from canvas_sdk.v1.data import CanvasUser
11
12
 
12
13
 
13
14
  @dataclasses.dataclass
@@ -23,6 +24,18 @@ class TargetType:
23
24
  return self.type._default_manager.filter(id=self.id).first() if self.type else None
24
25
 
25
26
 
27
+ @dataclasses.dataclass
28
+ class Actor:
29
+ """The actor that triggered the event."""
30
+
31
+ id: str | None
32
+
33
+ @cached_property
34
+ def instance(self) -> CanvasUser | None:
35
+ """Return the instance of the actor."""
36
+ return CanvasUser._default_manager.filter(dbid=self.id).first() if self.id else None
37
+
38
+
26
39
  class Event:
27
40
  """An event that occurs in the Canvas environment."""
28
41
 
@@ -46,6 +59,8 @@ class Event:
46
59
  self.name = EventType.Name(self.type)
47
60
  self.context = context
48
61
  self.target = TargetType(id=event_request.target, type=target_model)
62
+ self.actor = Actor(id=event_request.actor)
63
+ self.source = event_request.source
49
64
 
50
65
 
51
66
  __exports__ = ("TargetType", "Event")
@@ -3,6 +3,7 @@ from abc import ABC, abstractmethod
3
3
  from canvas_sdk.effects import Effect
4
4
  from canvas_sdk.events import EventType
5
5
  from canvas_sdk.handlers import BaseHandler
6
+ from canvas_sdk.handlers.utils import normalize_effects
6
7
 
7
8
 
8
9
  class Application(BaseHandler, ABC):
@@ -15,23 +16,22 @@ class Application(BaseHandler, ABC):
15
16
 
16
17
  def compute(self) -> list[Effect]:
17
18
  """Handle the application events."""
19
+ if self.event.target.id != self.identifier:
20
+ return []
18
21
  match self.event.type:
19
22
  case EventType.APPLICATION__ON_OPEN:
20
- return [self.on_open()] if self.event.target.id == self.identifier else []
23
+ return normalize_effects(self.on_open())
21
24
  case EventType.APPLICATION__ON_CONTEXT_CHANGE:
22
- if self.event.target.id == self.identifier:
23
- effect = self.on_context_change()
24
- return [effect] if effect is not None else []
25
- return []
25
+ return normalize_effects(self.on_context_change())
26
26
  case _:
27
27
  return []
28
28
 
29
29
  @abstractmethod
30
- def on_open(self) -> Effect:
30
+ def on_open(self) -> Effect | list[Effect]:
31
31
  """Handle the application open event."""
32
32
  ...
33
33
 
34
- def on_context_change(self) -> Effect | None:
34
+ def on_context_change(self) -> Effect | list[Effect] | None:
35
35
  """Handle the application context change event."""
36
36
  return None
37
37
 
@@ -1,7 +1,6 @@
1
1
  import inspect
2
2
  import json
3
3
  import re
4
- import traceback
5
4
  from abc import ABC
6
5
  from base64 import b64decode
7
6
  from collections.abc import Callable
@@ -323,9 +322,7 @@ class SimpleAPIBase(BaseHandler, ABC):
323
322
  else:
324
323
  raise AssertionError(f"Cannot handle event type {EventType.Name(self.event.type)}")
325
324
  except Exception as exception:
326
- for error_line_with_newlines in traceback.format_exception(exception):
327
- for error_line in error_line_with_newlines.split("\n"):
328
- log.error(error_line)
325
+ log.exception(f"Error handling '{EventType.Name(self.event.type)}' event")
329
326
 
330
327
  sentry_sdk.capture_exception(exception)
331
328
 
@@ -1,4 +1,3 @@
1
- import traceback
2
1
  from abc import ABC
3
2
  from functools import cached_property
4
3
  from typing import ClassVar
@@ -52,9 +51,7 @@ class WebSocketAPI(BaseHandler, ABC):
52
51
  else:
53
52
  raise AssertionError(f"Cannot handle event type {EventType.Name(self.event.type)}")
54
53
  except Exception as exception:
55
- for error_line_with_newlines in traceback.format_exception(exception):
56
- for error_line in error_line_with_newlines.split("\n"):
57
- log.error(error_line)
54
+ log.exception(f"Error handling '{EventType.Name(self.event.type)}' event")
58
55
 
59
56
  sentry_sdk.capture_exception(exception)
60
57
 
@@ -0,0 +1,14 @@
1
+ from canvas_sdk.effects import Effect
2
+
3
+
4
+ def normalize_effects(effects: Effect | list[Effect] | None) -> list[Effect]:
5
+ """Normalize effects to a list of Effect instances."""
6
+ if effects is None:
7
+ return []
8
+ if isinstance(effects, Effect):
9
+ return [effects]
10
+ if isinstance(effects, list):
11
+ return [e for e in effects if isinstance(e, Effect)]
12
+
13
+
14
+ __exports__ = ()
@@ -121,4 +121,5 @@ __exports__ = (
121
121
  "Draft7Validator",
122
122
  "ExtendedDraft7Validator",
123
123
  "from_yaml",
124
+ "QuestionnaireConfig",
124
125
  )
@@ -1,11 +1,25 @@
1
+ from functools import cache, lru_cache
1
2
  from pathlib import Path
2
3
  from typing import Any
3
4
 
4
- from django.template import Context, Template
5
+ from django.template.backends.django import get_installed_libraries
6
+ from django.template.engine import Engine
5
7
 
6
8
  from canvas_sdk.utils.plugins import plugin_context
7
9
 
8
10
 
11
+ @cache
12
+ def _installed_template_libraries() -> dict[str, str]:
13
+ """Cache Django's template tag libraries lookup."""
14
+ return get_installed_libraries()
15
+
16
+
17
+ @lru_cache(maxsize=5)
18
+ def _engine_for_plugin(plugin_dir: str) -> Engine:
19
+ """Create a Django template engine for the given plugin directory."""
20
+ return Engine(dirs=[plugin_dir], libraries=_installed_template_libraries())
21
+
22
+
9
23
  @plugin_context
10
24
  def render_to_string(
11
25
  template_name: str,
@@ -36,9 +50,8 @@ def render_to_string(
36
50
  elif not template_path.exists():
37
51
  raise FileNotFoundError(f"Template {template_name} not found.")
38
52
 
39
- template = Template(template_path.read_text())
40
-
41
- return template.render(Context(context))
53
+ engine = _engine_for_plugin(plugin_dir)
54
+ return engine.render_to_string(str(template_path), context=context)
42
55
 
43
56
 
44
57
  __exports__ = ("render_to_string",)