canvas 0.53.2__py3-none-any.whl → 0.54.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.

Potentially problematic release.


This version of canvas might be problematic. Click here for more details.

@@ -3,18 +3,21 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING, Any
4
4
 
5
5
  from canvas_sdk.caching.client import get_cache as get_cache_client
6
- from canvas_sdk.utils.plugins import plugin_only
6
+ from canvas_sdk.utils.plugins import plugin_context
7
7
  from settings import CANVAS_SDK_CACHE_TIMEOUT_SECONDS
8
8
 
9
9
  if TYPE_CHECKING:
10
10
  from canvas_sdk.caching.base import Cache
11
11
 
12
12
 
13
- @plugin_only
13
+ @plugin_context
14
14
  def get_cache(**kwargs: Any) -> Cache:
15
15
  """Get the cache client for plugins."""
16
- prefix = kwargs["plugin_name"]
17
- return get_cache_client("plugins", prefix, CANVAS_SDK_CACHE_TIMEOUT_SECONDS)
16
+ return get_cache_client(
17
+ driver="plugins",
18
+ prefix=kwargs["plugin_name"],
19
+ max_timeout_seconds=CANVAS_SDK_CACHE_TIMEOUT_SECONDS,
20
+ )
18
21
 
19
22
 
20
23
  __exports__ = ("get_cache",)
@@ -0,0 +1,137 @@
1
+ from enum import StrEnum
2
+ from typing import Any
3
+
4
+ from canvas_sdk.effects import EffectType, _BaseEffect
5
+
6
+
7
+ class PaymentProcessorMetadata(_BaseEffect):
8
+ """PaymentProcessorInfo effect class."""
9
+
10
+ class Meta:
11
+ effect_type = EffectType.REVENUE__PAYMENT_PROCESSOR__METADATA
12
+
13
+ class PaymentProcessorType(StrEnum):
14
+ """Enum for payment processor types."""
15
+
16
+ CARD = "card"
17
+
18
+ identifier: str
19
+ type: PaymentProcessorType
20
+
21
+ @property
22
+ def values(self) -> dict[str, Any]:
23
+ """Return the values of the PaymentProcessorMetadata."""
24
+ return {
25
+ "identifier": self.identifier,
26
+ "type": self.type.value,
27
+ }
28
+
29
+
30
+ class PaymentProcessorForm(_BaseEffect):
31
+ """PaymentProcessorForm effect class."""
32
+
33
+ class Meta:
34
+ effect_type = EffectType.REVENUE__PAYMENT_PROCESSOR__FORM
35
+
36
+ intent: str
37
+ content: str
38
+
39
+ @property
40
+ def values(self) -> dict[str, Any]:
41
+ """Return the values of the PaymentProcessorMetadata."""
42
+ return {
43
+ "intent": self.intent,
44
+ "content": self.content,
45
+ }
46
+
47
+
48
+ class CardTransaction(_BaseEffect):
49
+ """CardTransaction effect class."""
50
+
51
+ class Meta:
52
+ effect_type = EffectType.REVENUE__PAYMENT_PROCESSOR__CREDIT_CARD_TRANSACTION
53
+
54
+ success: bool
55
+ transaction_id: str | None
56
+ error_code: str | None = None
57
+ api_response: dict
58
+
59
+ @property
60
+ def values(self) -> dict[str, Any]:
61
+ """Return the values of the CreditCardTransaction."""
62
+ return {
63
+ "success": self.success,
64
+ "transaction_id": self.transaction_id,
65
+ "api_response": self.api_response,
66
+ "error_code": self.error_code,
67
+ }
68
+
69
+
70
+ class PaymentMethod(_BaseEffect):
71
+ """PaymentMethod effect class."""
72
+
73
+ class Meta:
74
+ effect_type = EffectType.REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHOD
75
+
76
+ payment_method_id: str
77
+ card_holder_name: str | None
78
+ brand: str
79
+ postal_code: str | None = None
80
+ country: str | None = None
81
+ expiration_year: int
82
+ expiration_month: int
83
+ card_last_four_digits: str
84
+
85
+ @property
86
+ def values(self) -> dict[str, Any]:
87
+ """Return the values of the PaymentMethod."""
88
+ return {
89
+ "payment_method_id": self.payment_method_id,
90
+ "card_holder_name": self.card_holder_name,
91
+ "brand": self.brand,
92
+ "postal_code": self.postal_code,
93
+ "country": self.country,
94
+ "expiration_year": self.expiration_year,
95
+ "expiration_month": self.expiration_month,
96
+ "card_last_four_digits": self.card_last_four_digits,
97
+ }
98
+
99
+
100
+ class AddPaymentMethodResponse(_BaseEffect):
101
+ """AddPaymentMethodResponse effect class."""
102
+
103
+ class Meta:
104
+ effect_type = EffectType.REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHOD__ADD_RESPONSE
105
+
106
+ success: bool
107
+
108
+ @property
109
+ def values(self) -> dict[str, Any]:
110
+ """Return the values of the AddPaymentMethodResponse."""
111
+ return {"success": self.success}
112
+
113
+
114
+ class RemovePaymentMethodResponse(_BaseEffect):
115
+ """RemovePaymentMethodResponse effect class."""
116
+
117
+ class Meta:
118
+ effect_type = EffectType.REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHOD__REMOVE_RESPONSE
119
+
120
+ success: bool
121
+
122
+ @property
123
+ def values(self) -> dict[str, Any]:
124
+ """Return the values of the RemovePaymentMethodResponse."""
125
+ return {
126
+ "success": self.success,
127
+ }
128
+
129
+
130
+ __exports__ = (
131
+ "PaymentProcessorMetadata",
132
+ "PaymentProcessorForm",
133
+ "CardTransaction",
134
+ "PaymentMethod",
135
+ "AddPaymentMethodResponse",
136
+ "RemovePaymentMethodResponse",
137
+ )
@@ -0,0 +1 @@
1
+ __exports__ = ()
@@ -0,0 +1,93 @@
1
+ import base64
2
+ from abc import ABC
3
+
4
+ from canvas_sdk.effects import Effect
5
+ from canvas_sdk.effects.payment_processor import PaymentProcessorMetadata
6
+ from canvas_sdk.events import EventType
7
+ from canvas_sdk.handlers import BaseHandler
8
+
9
+
10
+ class PaymentProcessor(BaseHandler, ABC):
11
+ """
12
+ Abstract Base class for payment processors.
13
+ """
14
+
15
+ RESPONDS_TO = [
16
+ EventType.Name(EventType.REVENUE__PAYMENT_PROCESSOR__LIST),
17
+ EventType.Name(EventType.REVENUE__PAYMENT_PROCESSOR__SELECTED),
18
+ EventType.Name(EventType.REVENUE__PAYMENT_PROCESSOR__CHARGE),
19
+ EventType.Name(EventType.REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHODS__LIST),
20
+ EventType.Name(EventType.REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHODS__ADD),
21
+ EventType.Name(EventType.REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHODS__REMOVE),
22
+ ]
23
+
24
+ TYPE: PaymentProcessorMetadata.PaymentProcessorType
25
+
26
+ @property
27
+ def identifier(self) -> str:
28
+ """The application identifier."""
29
+ identifier = f"{self.__class__.__module__}:{self.__class__.__qualname__}"
30
+
31
+ return base64.b64encode(identifier.encode("utf-8")).decode("utf-8")
32
+
33
+ def compute(self) -> list[Effect]:
34
+ """Compute the effects to be applied."""
35
+ if self.event.type == EventType.REVENUE__PAYMENT_PROCESSOR__LIST and (
36
+ "payment_type" not in self.event.context
37
+ or self.event.context["payment_type"] == self.TYPE
38
+ ):
39
+ return [self.metadata().apply()]
40
+ elif self.event.type == EventType.REVENUE__PAYMENT_PROCESSOR__SELECTED:
41
+ effects = self._on_payment_processor_selected()
42
+ return effects
43
+ elif self.event.type == EventType.REVENUE__PAYMENT_PROCESSOR__CHARGE:
44
+ effect = self._charge()
45
+ return [effect] if effect else []
46
+ elif self.event.type == EventType.REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHODS__LIST:
47
+ # This event is used to list payment methods, which may not be applicable for all processors.
48
+ # Subclasses should override this method if they support listing payment methods.
49
+ return self._payment_methods()
50
+ elif self.event.type == EventType.REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHODS__ADD:
51
+ effect = self._add_payment_method()
52
+ return [effect] if effect else []
53
+ elif self.event.type == EventType.REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHODS__REMOVE:
54
+ effect = self._remove_payment_method()
55
+ return [effect] if effect else []
56
+
57
+ return []
58
+
59
+ def metadata(self) -> PaymentProcessorMetadata:
60
+ """Return information about the payment processor."""
61
+ return PaymentProcessorMetadata(identifier=self.identifier, type=self.TYPE)
62
+
63
+ def _on_payment_processor_selected(self) -> list[Effect]:
64
+ """Handle the event when a payment processor is selected."""
65
+ # This method should be overridden by subclasses to handle specific logic
66
+ # when a payment processor is selected.
67
+ return []
68
+
69
+ def _charge(self) -> Effect | None:
70
+ """Handle the event when a charge is made."""
71
+ # This method should be overridden by subclasses to handle specific logic
72
+ # when a charge is made.
73
+ return None
74
+
75
+ def _payment_methods(self) -> list[Effect]:
76
+ """List payment methods for the processor."""
77
+ # This method should be overridden by subclasses if they support listing payment methods.
78
+ return []
79
+
80
+ def _add_payment_method(self) -> Effect | None:
81
+ """Handle the event when a payment method is added."""
82
+ # This method should be overridden by subclasses to handle specific logic
83
+ # when a payment method is added.
84
+ return None
85
+
86
+ def _remove_payment_method(self) -> Effect | None:
87
+ """Handle the event when a payment method is removed."""
88
+ # This method should be overridden by subclasses to handle specific logic
89
+ # when a payment method is removed.
90
+ return None
91
+
92
+
93
+ __exports__ = ("PaymentProcessor",)
@@ -0,0 +1,200 @@
1
+ from abc import ABC, abstractmethod
2
+ from decimal import Decimal
3
+ from enum import StrEnum
4
+
5
+ from canvas_sdk.effects import Effect
6
+ from canvas_sdk.effects.payment_processor import (
7
+ AddPaymentMethodResponse,
8
+ CardTransaction,
9
+ PaymentMethod,
10
+ PaymentProcessorForm,
11
+ PaymentProcessorMetadata,
12
+ RemovePaymentMethodResponse,
13
+ )
14
+ from canvas_sdk.handlers.payment_processors.base import PaymentProcessor
15
+ from canvas_sdk.v1.data import Patient
16
+
17
+
18
+ class CardPaymentProcessor(PaymentProcessor, ABC):
19
+ """Base Card Payment Processor Handler."""
20
+
21
+ TYPE = PaymentProcessorMetadata.PaymentProcessorType.CARD
22
+
23
+ class PaymentIntent(StrEnum):
24
+ """Enum for payment actions."""
25
+
26
+ ADD_CARD = "add_card"
27
+ PAY = "pay"
28
+
29
+ def _on_payment_processor_selected(self) -> list[Effect]:
30
+ """Handle the event when a payment processor is selected."""
31
+ if self.event.context.get("identifier") == self.identifier:
32
+ intent = self.event.context.get("intent")
33
+ effects = self.on_payment_processor_selected(intent=intent)
34
+ return [effect.apply() for effect in effects]
35
+
36
+ return []
37
+
38
+ def _charge(self) -> Effect | None:
39
+ """Handle the event when a charge is made."""
40
+ if self.event.context.get("identifier") == self.identifier:
41
+ patient = (
42
+ Patient.objects.get(id=self.event.context.get("patient", {}).get("id"))
43
+ if self.event.context.get("patient")
44
+ else None
45
+ )
46
+
47
+ amount = Decimal(str(self.event.context.get("amount")))
48
+ token = str(self.event.context.get("token"))
49
+ effect = self.charge(amount=amount, token=token, patient=patient)
50
+
51
+ return effect.apply() if effect else None
52
+
53
+ return None
54
+
55
+ def _payment_methods(self) -> list[Effect]:
56
+ """List payment methods for the card payment processor."""
57
+ if self.event.context.get("identifier") == self.identifier:
58
+ patient = (
59
+ Patient.objects.get(id=self.event.context.get("patient", {}).get("id"))
60
+ if self.event.context.get("patient")
61
+ else None
62
+ )
63
+ effects = self.payment_methods(patient=patient)
64
+ return [effect.apply() for effect in effects]
65
+
66
+ return []
67
+
68
+ def _add_payment_method(self) -> Effect | None:
69
+ """Handle the event when a card is added."""
70
+ if self.event.context.get("identifier") == self.identifier:
71
+ patient = (
72
+ Patient.objects.get(id=self.event.context.get("patient", {}).get("id"))
73
+ if self.event.context.get("patient")
74
+ else None
75
+ )
76
+
77
+ if not patient:
78
+ return None
79
+
80
+ token = str(self.event.context.get("token"))
81
+ effect = self.add_payment_method(token=token, patient=patient)
82
+ return effect.apply() if effect else None
83
+
84
+ return None
85
+
86
+ def _remove_payment_method(self) -> Effect | None:
87
+ """Handle the event when a payment method is removed."""
88
+ if self.event.context.get("identifier") == self.identifier:
89
+ patient = (
90
+ Patient.objects.get(id=self.event.context.get("patient", {}).get("id"))
91
+ if self.event.context.get("patient")
92
+ else None
93
+ )
94
+
95
+ if not patient:
96
+ return None
97
+
98
+ token = str(self.event.context.get("token"))
99
+ effect = self.remove_payment_method(token=token, patient=patient)
100
+ return effect.apply() if effect else None
101
+
102
+ return None
103
+
104
+ def on_payment_processor_selected(self, intent: str | None) -> list[PaymentProcessorForm]:
105
+ """Handle the event when a payment processor is selected."""
106
+ patient = (
107
+ Patient.objects.get(id=self.event.context.get("patient", {}).get("id"))
108
+ if self.event.context.get("patient")
109
+ else None
110
+ )
111
+ match intent:
112
+ case self.PaymentIntent.ADD_CARD:
113
+ return [self.add_card_form(patient)]
114
+ case self.PaymentIntent.PAY:
115
+ return [self.payment_form(patient)]
116
+ case None:
117
+ return [self.payment_form(patient), self.add_card_form(patient)]
118
+ case _:
119
+ return []
120
+
121
+ @abstractmethod
122
+ def payment_form(self, patient: Patient | None = None) -> PaymentProcessorForm:
123
+ """Return the payment form for the credit card processor.
124
+
125
+ Args:
126
+ patient (Patient | None): The patient for whom the payment is being processed.
127
+
128
+ Returns:
129
+ PaymentProcessorForm: The form for processing the payment.
130
+ """
131
+ raise NotImplementedError("Subclasses must implement the payment_form method.")
132
+
133
+ @abstractmethod
134
+ def add_card_form(self, patient: Patient | None = None) -> PaymentProcessorForm:
135
+ """Return the form for adding a card.
136
+
137
+ Args:
138
+ patient (Patient | None): The patient for whom the add is being added.
139
+
140
+ Returns:
141
+ PaymentProcessorForm: The form for adding a card.
142
+ """
143
+ raise NotImplementedError("Subclasses must implement the add_card_form method.")
144
+
145
+ @abstractmethod
146
+ def charge(
147
+ self, amount: Decimal, token: str, patient: Patient | None = None
148
+ ) -> CardTransaction:
149
+ """Charge a credit/debit card using the provided token.
150
+
151
+ Args:
152
+ amount (Decimal): The amount to charge.
153
+ token (str): The token representing the credit card.
154
+ patient (Patient | None): The patient for whom the charge is being made.
155
+
156
+ Returns:
157
+ CardTransaction: The result of the card transaction.
158
+ """
159
+ raise NotImplementedError("Subclasses must implement the charge method.")
160
+
161
+ @abstractmethod
162
+ def payment_methods(self, patient: Patient | None = None) -> list[PaymentMethod]:
163
+ """List payment methods for the card payment processor.
164
+
165
+ Args:
166
+ patient (Patient | None): The patient for whom the payment methods are being listed.
167
+
168
+ Returns:
169
+ list[PaymentMethod]: A list of payment methods available for the card payment processor.
170
+ """
171
+ raise NotImplementedError("Subclasses must implement the payment_methods method.")
172
+
173
+ @abstractmethod
174
+ def add_payment_method(self, token: str, patient: Patient) -> AddPaymentMethodResponse:
175
+ """Add a payment method for the card payment processor.
176
+
177
+ Args:
178
+ token (str): The token representing the payment method.
179
+ patient (Patient): The patient for whom the payment method is being added.
180
+
181
+ Returns:
182
+ AddPaymentMethodResponse: The response indicating the result of the addition operation.
183
+ """
184
+ raise NotImplementedError("Subclasses must implement the add_payment_method method.")
185
+
186
+ @abstractmethod
187
+ def remove_payment_method(self, token: str, patient: Patient) -> RemovePaymentMethodResponse:
188
+ """Remove a payment method for the card payment processor.
189
+
190
+ Args:
191
+ token (str): The token representing the payment method to be removed.
192
+ patient (Patient): The patient for whom the payment method is being removed.
193
+
194
+ Returns:
195
+ RemovePaymentMethodResponse: The response indicating the result of the removal operation.
196
+ """
197
+ raise NotImplementedError("Subclasses must implement the remove_payment_method method.")
198
+
199
+
200
+ __exports__ = ("CardPaymentProcessor",)
@@ -7,7 +7,7 @@ from typing import Any, TypedDict
7
7
  import yaml
8
8
  from jsonschema import Draft7Validator, validators
9
9
 
10
- from canvas_sdk.utils.plugins import plugin_only
10
+ from canvas_sdk.utils.plugins import plugin_context
11
11
 
12
12
 
13
13
  class Response(TypedDict):
@@ -75,7 +75,7 @@ def extend_with_defaults(validator_class: type[Draft7Validator]) -> type[Draft7V
75
75
  ExtendedDraft7Validator = extend_with_defaults(Draft7Validator)
76
76
 
77
77
 
78
- @plugin_only
78
+ @plugin_context
79
79
  def from_yaml(questionnaire_name: str, **kwargs: Any) -> QuestionnaireConfig | None:
80
80
  """Load a Questionnaire configuration from a YAML file.
81
81
 
@@ -3,12 +3,14 @@ from typing import Any
3
3
 
4
4
  from django.template import Context, Template
5
5
 
6
- from canvas_sdk.utils.plugins import plugin_only
6
+ from canvas_sdk.utils.plugins import plugin_context
7
7
 
8
8
 
9
- @plugin_only
9
+ @plugin_context
10
10
  def render_to_string(
11
- template_name: str, context: dict[str, Any] | None = None, **kwargs: Any
11
+ template_name: str,
12
+ context: dict[str, Any] | None = None,
13
+ **kwargs: Any,
12
14
  ) -> str | None:
13
15
  """Load a template and render it with the given context.
14
16
 
@@ -8,19 +8,39 @@ from canvas_sdk.utils.metrics import measured
8
8
  from settings import PLUGIN_DIRECTORY
9
9
 
10
10
 
11
+ def find_plugin_ancestor(frame: FrameType | None, max_depth: int = 10) -> FrameType | None:
12
+ """
13
+ Recurse backwards to find any plugin ancestor of this frame.
14
+ """
15
+ parent_frame = frame.f_back if frame else None
16
+
17
+ if not parent_frame:
18
+ return None
19
+
20
+ if max_depth == 0:
21
+ return None
22
+
23
+ if "__is_plugin__" in parent_frame.f_globals:
24
+ return parent_frame
25
+
26
+ return find_plugin_ancestor(frame=parent_frame, max_depth=max_depth - 1)
27
+
28
+
11
29
  @measured
12
- def plugin_only(func: Callable[..., Any]) -> Callable[..., Any]:
30
+ def plugin_context(func: Callable[..., Any]) -> Callable[..., Any]:
13
31
  """Decorator to restrict a function's execution to plugins only."""
14
32
 
15
33
  def wrapper(*args: Any, **kwargs: Any) -> Any:
16
- current_frame = inspect.currentframe()
17
- caller = current_frame.f_back if current_frame else None
34
+ plugin_frame = find_plugin_ancestor(inspect.currentframe())
18
35
 
19
- if not caller or "__is_plugin__" not in caller.f_globals:
20
- return None
36
+ if not plugin_frame:
37
+ raise RuntimeError(
38
+ "Method that expected plugin context was called from outside a plugin."
39
+ )
21
40
 
22
- plugin_name = caller.f_globals["__name__"].split(".")[0]
41
+ plugin_name = plugin_frame.f_globals["__name__"].split(".")[0]
23
42
  plugin_dir = Path(PLUGIN_DIRECTORY) / plugin_name
43
+
24
44
  kwargs["plugin_name"] = plugin_name
25
45
  kwargs["plugin_dir"] = plugin_dir.resolve()
26
46
 
@@ -30,24 +50,17 @@ def plugin_only(func: Callable[..., Any]) -> Callable[..., Any]:
30
50
 
31
51
 
32
52
  @measured
33
- def is_plugin_caller(depth: int = 10, frame: FrameType | None = None) -> tuple[bool, str | None]:
53
+ def is_plugin_caller() -> tuple[bool, str | None]:
34
54
  """Check if a function is called from a plugin."""
35
- current_frame = frame or inspect.currentframe()
36
- caller = current_frame.f_back if current_frame else None
37
-
38
- if not caller:
39
- return False, None
55
+ plugin_frame = find_plugin_ancestor(inspect.currentframe())
40
56
 
41
- if "__is_plugin__" not in caller.f_globals:
42
- if depth > 0:
43
- return is_plugin_caller(frame=caller, depth=depth - 1)
44
- else:
45
- return False, None
57
+ if plugin_frame:
58
+ module = plugin_frame.f_globals.get("__name__")
59
+ qualname = plugin_frame.f_code.co_qualname
46
60
 
47
- module = caller.f_globals.get("__name__")
48
- qualname = caller.f_code.co_qualname
61
+ return True, f"{module}.{qualname}"
49
62
 
50
- return True, f"{module}.{qualname}"
63
+ return False, None
51
64
 
52
65
 
53
66
  __exports__ = ()
@@ -99,6 +99,13 @@ class Patient(Model):
99
99
  def __str__(self) -> str:
100
100
  return f"{self.first_name} {self.last_name}"
101
101
 
102
+ @property
103
+ def full_name(self) -> str:
104
+ """Returns the patient's full name."""
105
+ return " ".join(
106
+ n for n in (self.first_name, self.middle_name, self.last_name, self.suffix) if n
107
+ )
108
+
102
109
  def age_at(self, time: arrow.Arrow) -> float:
103
110
  """Given a datetime, returns what the patient's age would be at that datetime."""
104
111
  age = float(0)
@@ -279,6 +279,14 @@
279
279
  "canvas_sdk.effects.patient_profile_configuration": [
280
280
  "PatientProfileConfiguration"
281
281
  ],
282
+ "canvas_sdk.effects.payment_processor": [
283
+ "AddPaymentMethodResponse",
284
+ "CardTransaction",
285
+ "PaymentMethod",
286
+ "PaymentProcessorForm",
287
+ "PaymentProcessorMetadata",
288
+ "RemovePaymentMethodResponse"
289
+ ],
282
290
  "canvas_sdk.effects.protocol_card": [
283
291
  "ProtocolCard",
284
292
  "Recommendation"
@@ -364,6 +372,12 @@
364
372
  "canvas_sdk.handlers.cron_task": [
365
373
  "CronTask"
366
374
  ],
375
+ "canvas_sdk.handlers.payment_processors.base": [
376
+ "PaymentProcessor"
377
+ ],
378
+ "canvas_sdk.handlers.payment_processors.card": [
379
+ "CardPaymentProcessor"
380
+ ],
367
381
  "canvas_sdk.handlers.simple_api": [
368
382
  "APIKeyAuthMixin",
369
383
  "APIKeyCredentials",
@@ -147,6 +147,8 @@ def download_plugin(plugin_package: str) -> Generator[Path, None, None]:
147
147
 
148
148
  with open(download_path, "wb") as download_file:
149
149
  response = requests.request(method=method, url=f"https://{host}{path}", headers=headers)
150
+ response.raise_for_status()
151
+
150
152
  download_file.write(response.content)
151
153
 
152
154
  yield download_path
@@ -168,7 +170,8 @@ def install_plugin(plugin_name: str, attributes: PluginAttributes) -> None:
168
170
 
169
171
  install_plugin_secrets(plugin_name=plugin_name, secrets=attributes["secrets"])
170
172
  except Exception as e:
171
- log.error(f'Failed to install plugin "{plugin_name}", version {attributes["version"]}')
173
+ log.error(f'Failed to install plugin "{plugin_name}", version {attributes["version"]}: {e}')
174
+
172
175
  sentry_sdk.capture_exception(e)
173
176
 
174
177
  raise PluginInstallationError() from e
@@ -1,3 +1,4 @@
1
+ import base64
1
2
  import json
2
3
  import os
3
4
  import pathlib
@@ -213,6 +214,28 @@ class PluginRunner(PluginRunnerServicer):
213
214
  # respond to SimpleAPI request events are not relevant
214
215
  plugin_name = event.context["plugin_name"]
215
216
  relevant_plugins = [p for p in relevant_plugins if p.startswith(f"{plugin_name}:")]
217
+ elif event_type in {
218
+ EventType.REVENUE__PAYMENT_PROCESSOR__CHARGE,
219
+ EventType.REVENUE__PAYMENT_PROCESSOR__SELECTED,
220
+ EventType.REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHODS__LIST,
221
+ EventType.REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHODS__ADD,
222
+ EventType.REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHODS__REMOVE,
223
+ }:
224
+ # The target plugin's name will be part of the payment processor identifier, so other plugins that
225
+ # respond to payment processor charge events are not relevant
226
+ try:
227
+ plugin_name = (
228
+ base64.b64decode(event.context["identifier"]).decode("utf-8").split(".")[0]
229
+ )
230
+ relevant_plugins = [
231
+ p for p in relevant_plugins if p.startswith(f"{plugin_name}:")
232
+ ]
233
+ except Exception as ex:
234
+ log.error(
235
+ f"Failed to decode identifier for event {event_name} with context {event.context}"
236
+ )
237
+ sentry_sdk.capture_exception(ex)
238
+ relevant_plugins = []
216
239
 
217
240
  effect_list = []
218
241
 
plugin_runner/sandbox.py CHANGED
@@ -225,6 +225,7 @@ THIRD_PARTY_MODULES = {
225
225
  },
226
226
  "pydantic": {
227
227
  "BaseModel",
228
+ "ConfigDict",
228
229
  "conint",
229
230
  "constr",
230
231
  "Field",