mpt-extension-sdk 4.5.0__py3-none-any.whl → 5.17.2__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 (42) hide show
  1. mpt_extension_sdk/airtable/wrap_http_error.py +10 -6
  2. mpt_extension_sdk/constants.py +2 -0
  3. mpt_extension_sdk/core/events/dataclasses.py +2 -0
  4. mpt_extension_sdk/core/events/registry.py +8 -3
  5. mpt_extension_sdk/core/extension.py +2 -0
  6. mpt_extension_sdk/core/security.py +35 -27
  7. mpt_extension_sdk/core/utils.py +2 -0
  8. mpt_extension_sdk/flows/context.py +12 -1
  9. mpt_extension_sdk/flows/pipeline.py +11 -2
  10. mpt_extension_sdk/key_vault/base.py +36 -25
  11. mpt_extension_sdk/mpt_http/base.py +15 -3
  12. mpt_extension_sdk/mpt_http/mpt.py +73 -12
  13. mpt_extension_sdk/mpt_http/utils.py +1 -0
  14. mpt_extension_sdk/mpt_http/wrap_http_error.py +30 -8
  15. mpt_extension_sdk/runtime/__init__.py +1 -0
  16. mpt_extension_sdk/runtime/commands/django.py +5 -5
  17. mpt_extension_sdk/runtime/commands/run.py +2 -4
  18. mpt_extension_sdk/runtime/djapp/apps.py +7 -1
  19. mpt_extension_sdk/runtime/djapp/conf/__init__.py +2 -3
  20. mpt_extension_sdk/runtime/djapp/conf/default.py +9 -15
  21. mpt_extension_sdk/runtime/djapp/management/commands/consume_events.py +12 -7
  22. mpt_extension_sdk/runtime/djapp/middleware.py +5 -3
  23. mpt_extension_sdk/runtime/errors.py +5 -0
  24. mpt_extension_sdk/runtime/events/dispatcher.py +21 -19
  25. mpt_extension_sdk/runtime/events/producers.py +29 -25
  26. mpt_extension_sdk/runtime/events/utils.py +6 -5
  27. mpt_extension_sdk/runtime/initializer.py +4 -5
  28. mpt_extension_sdk/runtime/logging.py +11 -2
  29. mpt_extension_sdk/runtime/master.py +25 -17
  30. mpt_extension_sdk/runtime/swoext.py +10 -8
  31. mpt_extension_sdk/runtime/tracer.py +2 -0
  32. mpt_extension_sdk/runtime/utils.py +37 -38
  33. mpt_extension_sdk/runtime/workers.py +14 -8
  34. mpt_extension_sdk/swo_rql/query_builder.py +17 -14
  35. mpt_extension_sdk-5.17.2.dist-info/METADATA +39 -0
  36. mpt_extension_sdk-5.17.2.dist-info/RECORD +54 -0
  37. {mpt_extension_sdk-4.5.0.dist-info → mpt_extension_sdk-5.17.2.dist-info}/WHEEL +1 -1
  38. mpt_extension_sdk-5.17.2.dist-info/entry_points.txt +5 -0
  39. mpt_extension_sdk-4.5.0.dist-info/METADATA +0 -45
  40. mpt_extension_sdk-4.5.0.dist-info/RECORD +0 -53
  41. mpt_extension_sdk-4.5.0.dist-info/entry_points.txt +0 -6
  42. {mpt_extension_sdk-4.5.0.dist-info → mpt_extension_sdk-5.17.2.dist-info}/licenses/LICENSE +0 -0
@@ -5,10 +5,12 @@ from requests import HTTPError, JSONDecodeError
5
5
 
6
6
 
7
7
  class AirTableError(Exception):
8
- pass
8
+ """Base class for all AirTable API exceptions."""
9
9
 
10
10
 
11
11
  class AirTableHttpError(AirTableError):
12
+ """Exception raised for HTTP errors in the AirTable API."""
13
+
12
14
  def __init__(self, status_code: int, content: str):
13
15
  self.status_code = status_code
14
16
  self.content = content
@@ -16,6 +18,8 @@ class AirTableHttpError(AirTableError):
16
18
 
17
19
 
18
20
  class AirTableAPIError(AirTableHttpError):
21
+ """Exception raised for errors in the AirTable API."""
22
+
19
23
  def __init__(self, status_code: int, payload) -> None:
20
24
  super().__init__(status_code, json.dumps(payload))
21
25
  self.payload = payload
@@ -30,16 +34,16 @@ class AirTableAPIError(AirTableHttpError):
30
34
 
31
35
 
32
36
  def wrap_airtable_http_error(func):
37
+ """Wrap a function to catch AirTable HTTP errors."""
38
+
33
39
  @wraps(func)
34
40
  def _wrapper(*args, **kwargs):
35
41
  try:
36
42
  return func(*args, **kwargs)
37
- except HTTPError as e:
43
+ except HTTPError as err:
38
44
  try:
39
- raise AirTableAPIError(e.response.status_code, e.response.json())
45
+ raise AirTableAPIError(err.response.status_code, err.response.json())
40
46
  except JSONDecodeError:
41
- raise AirTableHttpError(
42
- e.response.status_code, e.response.content.decode()
43
- )
47
+ raise AirTableHttpError(err.response.status_code, err.response.content.decode())
44
48
 
45
49
  return _wrapper
@@ -9,3 +9,5 @@ ERR_DJANGO_SETTINGS_MODULE_TEXT = (
9
9
  "DJANGO_SETTINGS_MODULE environment variable is not set. "
10
10
  "Please set it to your Django settings module before running this command."
11
11
  )
12
+ POOL_MAX_SIZE = 36
13
+ GRADIENT_HEX_BASE = 16
@@ -11,6 +11,8 @@ EventType = Annotated[Literal[EVENT_TYPES], Doc("Unique identifier of the event
11
11
 
12
12
  @dataclass
13
13
  class Event:
14
+ """Represents an event."""
15
+
14
16
  id: Annotated[str, Doc("The unique identifier of the event.")]
15
17
  type: EventType
16
18
  data: Annotated[Mapping | Sequence, Doc("Event data.")]
@@ -1,12 +1,14 @@
1
- from collections.abc import MutableMapping, Sequence
2
- from typing import Any, Callable
1
+ from collections.abc import Callable, MutableMapping, Sequence
2
+ from typing import Any
3
3
 
4
- from .dataclasses import Event, EventType
4
+ from mpt_extension_sdk.core.events.dataclasses import Event, EventType
5
5
 
6
6
  EventListener = Callable[[Any, Event], None]
7
7
 
8
8
 
9
9
  class EventsRegistry:
10
+ """Registry for event listeners."""
11
+
10
12
  def __init__(
11
13
  self,
12
14
  ) -> None:
@@ -44,13 +46,16 @@ class EventsRegistry:
44
46
  self,
45
47
  event_type: EventType,
46
48
  ) -> EventListener | None:
49
+ """Get the listener for a specific event type."""
47
50
  return self.listeners.get(event_type)
48
51
 
49
52
  def get_registered_types(self) -> Sequence[str]:
53
+ """Get a list of all registered event types."""
50
54
  return list(self.listeners.keys())
51
55
 
52
56
  def is_event_supported(
53
57
  self,
54
58
  event_type: EventType,
55
59
  ) -> bool:
60
+ """Check if an event type is supported."""
56
61
  return event_type in self.listeners
@@ -4,6 +4,8 @@ from mpt_extension_sdk.core.events.registry import EventsRegistry
4
4
 
5
5
 
6
6
  class Extension:
7
+ """Base class for all extensions."""
8
+
7
9
  def __init__(
8
10
  self,
9
11
  /,
@@ -1,6 +1,6 @@
1
1
  import logging
2
- from collections.abc import Mapping
3
- from typing import Any, Callable
2
+ from collections.abc import Callable, Mapping
3
+ from typing import Any, ClassVar
4
4
 
5
5
  import jwt
6
6
  from django.http import HttpRequest
@@ -13,7 +13,9 @@ logger = logging.getLogger(__name__)
13
13
 
14
14
 
15
15
  class JWTAuth(HttpBearer):
16
- JWT_ALGOS = [SECURITY_ALGORITHM]
16
+ """JWT authentication using JSON Web Tokens."""
17
+
18
+ jwt_algos: ClassVar[list[str]] = [SECURITY_ALGORITHM]
17
19
 
18
20
  def __init__(
19
21
  self,
@@ -23,28 +25,34 @@ class JWTAuth(HttpBearer):
23
25
  super().__init__()
24
26
 
25
27
  def authenticate(self, request: HttpRequest, token: str) -> Any | None:
28
+ """Authenticate the request using the provided JWT token."""
26
29
  try:
27
- claims = jwt.decode(
28
- token,
29
- options={
30
- "verify_signature": False,
31
- "verify_aud": False,
32
- },
33
- algorithms=self.JWT_ALGOS,
34
- )
35
- secret = self.secret_callback(request.client, claims)
36
- if not secret:
37
- return
38
- jwt.decode(
39
- token,
40
- secret,
41
- options={
42
- "verify_aud": False,
43
- },
44
- algorithms=self.JWT_ALGOS,
45
- )
46
- request.jwt_claims = claims
47
- return claims
48
-
49
- except jwt.PyJWTError as e:
50
- logger.error(f"Call cannot be authenticated: {str(e)}")
30
+ request.jwt_claims = self.get_claims(request, token)
31
+ except jwt.PyJWTError as err:
32
+ logger.exception("Call cannot be authenticated: %r", err) # noqa: TRY401
33
+ else:
34
+ return request.jwt_claims
35
+
36
+ def get_claims(self, request, token):
37
+ """Extract JWT claims from the token."""
38
+ claims = jwt.decode(
39
+ token,
40
+ options={
41
+ "verify_signature": False,
42
+ "verify_aud": False,
43
+ },
44
+ algorithms=self.jwt_algos,
45
+ )
46
+ secret = self.secret_callback(request.client, claims)
47
+ if not secret:
48
+ return None
49
+ jwt.decode(
50
+ token,
51
+ secret,
52
+ options={
53
+ "verify_aud": False,
54
+ },
55
+ algorithms=self.jwt_algos,
56
+ )
57
+ request.jwt_claims = claims
58
+ return claims
@@ -4,6 +4,7 @@ from mpt_extension_sdk.mpt_http.base import MPTClient
4
4
 
5
5
 
6
6
  def setup_client():
7
+ """Set up the main client."""
7
8
  return MPTClient(
8
9
  f"{settings.MPT_API_BASE_URL}/v1/",
9
10
  settings.MPT_API_TOKEN,
@@ -11,6 +12,7 @@ def setup_client():
11
12
 
12
13
 
13
14
  def setup_operations_client():
15
+ """Set up the operations client."""
14
16
  return MPTClient(
15
17
  f"{settings.MPT_API_BASE_URL}/v1/",
16
18
  settings.MPT_API_TOKEN_OPERATIONS,
@@ -7,33 +7,44 @@ ORDER_TYPE_TERMINATION = "Termination"
7
7
 
8
8
  @dataclass
9
9
  class Context:
10
+ """Represents the context for an order."""
11
+
10
12
  order: dict
11
13
 
12
14
  @property
13
15
  def order_id(self):
16
+ """Return the order ID."""
14
17
  return self.order.get("id", None)
15
18
 
16
19
  @property
17
20
  def order_type(self):
21
+ """Return the order type."""
18
22
  return self.order.get("type", None)
19
23
 
20
24
  @property
21
25
  def product_id(self):
26
+ """Return the product ID."""
22
27
  return self.order.get("product", {}).get("id", None)
23
28
 
24
29
  def is_purchase_order(self):
30
+ """Check if the order is a purchase order."""
25
31
  return self.order["type"] == ORDER_TYPE_PURCHASE
26
32
 
27
33
  def is_change_order(self):
34
+ """Check if the order is a change order."""
28
35
  return self.order["type"] == ORDER_TYPE_CHANGE
29
36
 
30
37
  def is_termination_order(self):
38
+ """Check if the order is a termination order."""
31
39
  return self.order["type"] == ORDER_TYPE_TERMINATION
32
40
 
33
41
  @classmethod
34
42
  def from_context(cls, context):
43
+ """Create a new Context instance from an existing one."""
35
44
  base_data = asdict(context)
36
45
  return cls(**base_data)
37
46
 
38
47
  def __str__(self):
39
- return f"Context: {self.order.get("id", None)} {self.order.get("type", None)}"
48
+ order_id = self.order.get("id", None)
49
+ order_type = self.order.get("type", None)
50
+ return f"Context: {order_id} {order_type}"
@@ -1,5 +1,5 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Callable
2
+ from collections.abc import Callable
3
3
 
4
4
  from mpt_extension_sdk.flows.context import Context
5
5
  from mpt_extension_sdk.mpt_http.base import MPTClient
@@ -8,6 +8,8 @@ NextStep = Callable[[MPTClient, Context], None]
8
8
 
9
9
 
10
10
  class Step(ABC):
11
+ """Abstract base class for pipeline steps."""
12
+
11
13
  @abstractmethod
12
14
  def __call__(
13
15
  self,
@@ -15,7 +17,8 @@ class Step(ABC):
15
17
  context: Context,
16
18
  next_step: NextStep,
17
19
  ) -> None:
18
- raise NotImplementedError() # pragma: no cover
20
+ """Execute the step."""
21
+ raise NotImplementedError # pragma: no cover
19
22
 
20
23
 
21
24
  def _default_error_handler(error: Exception, context: Context, next_step: NextStep):
@@ -23,11 +26,14 @@ def _default_error_handler(error: Exception, context: Context, next_step: NextSt
23
26
 
24
27
 
25
28
  class Cursor:
29
+ """A cursor for navigating through pipeline steps."""
30
+
26
31
  def __init__(self, steps, error_handler):
27
32
  self.queue = steps
28
33
  self.error_handler = error_handler
29
34
 
30
35
  def __call__(self, client: MPTClient, context: Context):
36
+ """Execute the next step in the pipeline."""
31
37
  if not self.queue:
32
38
  return
33
39
  current_step = self.queue[0]
@@ -40,10 +46,13 @@ class Cursor:
40
46
 
41
47
 
42
48
  class Pipeline:
49
+ """A pipeline for processing steps."""
50
+
43
51
  def __init__(self, *steps):
44
52
  self.queue = steps
45
53
 
46
54
  def run(self, client: MPTClient, context: Context, error_handler=None):
55
+ """Run the pipeline."""
47
56
  execute = Cursor(self.queue, error_handler or _default_error_handler)
48
57
  return execute(client, context)
49
58
 
@@ -12,6 +12,8 @@ logger = logging.getLogger(__name__)
12
12
 
13
13
 
14
14
  class KeyVault(Session):
15
+ """A client for interacting with Azure Key Vault."""
16
+
15
17
  def __init__(self, key_vault_name: str):
16
18
  """
17
19
  Initialize the KeyVault client with the provided Key Vault name.
@@ -34,15 +36,10 @@ class KeyVault(Session):
34
36
  str: The value of the secret.
35
37
  """
36
38
  try:
37
- client = self._get_key_vault_client(self.key_vault_name)
38
-
39
- # Retrieve the secret from the Key Vault
40
- secret = client.get_secret(secret_name)
41
-
42
- return secret.value
43
- except ResourceNotFoundError as e:
44
- logger.error(
45
- f"Secret '{secret_name}' not found in Key Vault '{self.key_vault_name}': {e}"
39
+ return self._get_secret_from_key_vault(secret_name)
40
+ except ResourceNotFoundError:
41
+ logger.exception(
42
+ "Secret '%s' not found in Key Vault '%s'", secret_name, self.key_vault_name
46
43
  )
47
44
  return None
48
45
 
@@ -58,21 +55,39 @@ class KeyVault(Session):
58
55
  Returns:
59
56
  None
60
57
  """
61
-
62
58
  # Set the secret in the Key Vault
63
59
  try:
64
- client = self._get_key_vault_client(self.key_vault_name)
60
+ return self._set_secret_for_key_vault(secret_name, secret_value)
61
+ except HttpResponseError as err:
62
+ logger.exception(
63
+ "Failed to set secret '%s' in Key Vault '%s': %s",
64
+ secret_name,
65
+ self.key_vault_name,
66
+ err, # noqa: TRY401
67
+ )
68
+ return None
65
69
 
66
- client.set_secret(secret_name, secret_value)
70
+ def _get_secret_from_key_vault(self, secret_name: str):
71
+ """
72
+ Retrieve a secret from the Azure Key Vault.
67
73
 
68
- updated_secret = client.get_secret(secret_name)
74
+ Args:
75
+ secret_name (str): The name of the secret to retrieve.
69
76
 
70
- return updated_secret.value
71
- except HttpResponseError as e:
72
- logger.error(
73
- f"Failed to set secret '{secret_name}' in Key Vault '{self.key_vault_name}': {e}"
74
- )
75
- return None
77
+ Returns:
78
+ str: The value of the secret.
79
+ """
80
+ client = self._get_key_vault_client(self.key_vault_name)
81
+
82
+ # Retrieve the secret from the Key Vault
83
+ return client.get_secret(secret_name).value
84
+
85
+ def _set_secret_for_key_vault(self, secret_name: str, secret_value: str):
86
+ client = self._get_key_vault_client(self.key_vault_name)
87
+
88
+ client.set_secret(secret_name, secret_value)
89
+
90
+ return client.get_secret(secret_name).value
76
91
 
77
92
  def _get_key_vault_url(self, key_vault_name: str): # pragma: no cover
78
93
  """
@@ -84,7 +99,6 @@ class KeyVault(Session):
84
99
  Returns:
85
100
  str: The URL of the Azure Key Vault.
86
101
  """
87
-
88
102
  # Construct the Key Vault URL
89
103
  return f"https://{key_vault_name}.vault.azure.net/"
90
104
 
@@ -93,18 +107,15 @@ class KeyVault(Session):
93
107
  Create a Key Vault client using the provided Key Vault URL and secret name.
94
108
 
95
109
  Args:
96
- key_vault_url (str): The URL of the Azure Key Vault.
110
+ key_vault_name (str): The name of the Azure Key Vault.
97
111
 
98
112
  Returns:
99
113
  SecretClient: An instance of the SecretClient for the specified Key Vault.
100
114
  """
101
-
102
115
  # Get the Key Vault URL
103
116
  key_vault_url = self._get_key_vault_url(key_vault_name)
104
117
  # Create a credential object using DefaultAzureCredential
105
118
  credential = DefaultAzureCredential()
106
119
 
107
120
  # Create a Key Vault client using the credential
108
- client = SecretClient(vault_url=key_vault_url, credential=credential)
109
-
110
- return client
121
+ return SecretClient(vault_url=key_vault_url, credential=credential)
@@ -3,10 +3,12 @@ from urllib.parse import urljoin
3
3
  from requests import Session
4
4
  from requests.adapters import HTTPAdapter, Retry
5
5
 
6
- from mpt_extension_sdk.constants import USER_AGENT
6
+ from mpt_extension_sdk.constants import POOL_MAX_SIZE, USER_AGENT
7
7
 
8
8
 
9
9
  class MPTClient(Session):
10
+ """Client for interacting with the MPT API."""
11
+
10
12
  def __init__(self, base_url, api_token):
11
13
  super().__init__()
12
14
  retries = Retry(
@@ -18,7 +20,14 @@ class MPTClient(Session):
18
20
  "http://",
19
21
  HTTPAdapter(
20
22
  max_retries=retries,
21
- pool_maxsize=36,
23
+ pool_maxsize=POOL_MAX_SIZE,
24
+ ),
25
+ )
26
+ self.mount(
27
+ "https://",
28
+ HTTPAdapter(
29
+ max_retries=retries,
30
+ pool_maxsize=POOL_MAX_SIZE,
22
31
  ),
23
32
  )
24
33
  self.headers.update(
@@ -27,17 +36,20 @@ class MPTClient(Session):
27
36
  "Authorization": f"Bearer {api_token}",
28
37
  },
29
38
  )
30
- self.base_url = f"{base_url}/" if base_url[-1] != "/" else base_url
39
+ self.base_url = base_url if base_url[-1] == "/" else f"{base_url}/"
31
40
  self.api_token = api_token
32
41
 
33
42
  def request(self, method, url, *args, **kwargs):
43
+ """Send a request to the API."""
34
44
  url = self.join_url(url)
35
45
  return super().request(method, url, *args, **kwargs)
36
46
 
37
47
  def prepare_request(self, request, *args, **kwargs):
48
+ """Prepare the request by joining the base URL."""
38
49
  request.url = self.join_url(request.url)
39
50
  return super().prepare_request(request, *args, **kwargs)
40
51
 
41
52
  def join_url(self, url):
53
+ """Join the base URL with the given URL."""
42
54
  url = url[1:] if url[0] == "/" else url
43
55
  return urljoin(self.base_url, url)