mpt-extension-sdk 4.5.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 (53) hide show
  1. mpt_extension_sdk/__init__.py +0 -0
  2. mpt_extension_sdk/airtable/__init__.py +0 -0
  3. mpt_extension_sdk/airtable/wrap_http_error.py +45 -0
  4. mpt_extension_sdk/constants.py +11 -0
  5. mpt_extension_sdk/core/__init__.py +0 -0
  6. mpt_extension_sdk/core/events/__init__.py +0 -0
  7. mpt_extension_sdk/core/events/dataclasses.py +16 -0
  8. mpt_extension_sdk/core/events/registry.py +56 -0
  9. mpt_extension_sdk/core/extension.py +12 -0
  10. mpt_extension_sdk/core/security.py +50 -0
  11. mpt_extension_sdk/core/utils.py +17 -0
  12. mpt_extension_sdk/flows/__init__.py +0 -0
  13. mpt_extension_sdk/flows/context.py +39 -0
  14. mpt_extension_sdk/flows/pipeline.py +51 -0
  15. mpt_extension_sdk/key_vault/__init__.py +0 -0
  16. mpt_extension_sdk/key_vault/base.py +110 -0
  17. mpt_extension_sdk/mpt_http/__init__.py +0 -0
  18. mpt_extension_sdk/mpt_http/base.py +43 -0
  19. mpt_extension_sdk/mpt_http/mpt.py +530 -0
  20. mpt_extension_sdk/mpt_http/utils.py +2 -0
  21. mpt_extension_sdk/mpt_http/wrap_http_error.py +68 -0
  22. mpt_extension_sdk/runtime/__init__.py +10 -0
  23. mpt_extension_sdk/runtime/commands/__init__.py +0 -0
  24. mpt_extension_sdk/runtime/commands/django.py +42 -0
  25. mpt_extension_sdk/runtime/commands/run.py +44 -0
  26. mpt_extension_sdk/runtime/djapp/__init__.py +0 -0
  27. mpt_extension_sdk/runtime/djapp/apps.py +46 -0
  28. mpt_extension_sdk/runtime/djapp/conf/__init__.py +12 -0
  29. mpt_extension_sdk/runtime/djapp/conf/default.py +225 -0
  30. mpt_extension_sdk/runtime/djapp/conf/urls.py +9 -0
  31. mpt_extension_sdk/runtime/djapp/management/__init__.py +0 -0
  32. mpt_extension_sdk/runtime/djapp/management/commands/__init__.py +0 -0
  33. mpt_extension_sdk/runtime/djapp/management/commands/consume_events.py +38 -0
  34. mpt_extension_sdk/runtime/djapp/middleware.py +21 -0
  35. mpt_extension_sdk/runtime/events/__init__.py +0 -0
  36. mpt_extension_sdk/runtime/events/dispatcher.py +83 -0
  37. mpt_extension_sdk/runtime/events/producers.py +108 -0
  38. mpt_extension_sdk/runtime/events/utils.py +85 -0
  39. mpt_extension_sdk/runtime/initializer.py +62 -0
  40. mpt_extension_sdk/runtime/logging.py +36 -0
  41. mpt_extension_sdk/runtime/master.py +136 -0
  42. mpt_extension_sdk/runtime/swoext.py +69 -0
  43. mpt_extension_sdk/runtime/tracer.py +18 -0
  44. mpt_extension_sdk/runtime/utils.py +148 -0
  45. mpt_extension_sdk/runtime/workers.py +90 -0
  46. mpt_extension_sdk/swo_rql/__init__.py +5 -0
  47. mpt_extension_sdk/swo_rql/constants.py +7 -0
  48. mpt_extension_sdk/swo_rql/query_builder.py +392 -0
  49. mpt_extension_sdk-4.5.0.dist-info/METADATA +45 -0
  50. mpt_extension_sdk-4.5.0.dist-info/RECORD +53 -0
  51. mpt_extension_sdk-4.5.0.dist-info/WHEEL +4 -0
  52. mpt_extension_sdk-4.5.0.dist-info/entry_points.txt +6 -0
  53. mpt_extension_sdk-4.5.0.dist-info/licenses/LICENSE +201 -0
File without changes
File without changes
@@ -0,0 +1,45 @@
1
+ import json
2
+ from functools import wraps
3
+
4
+ from requests import HTTPError, JSONDecodeError
5
+
6
+
7
+ class AirTableError(Exception):
8
+ pass
9
+
10
+
11
+ class AirTableHttpError(AirTableError):
12
+ def __init__(self, status_code: int, content: str):
13
+ self.status_code = status_code
14
+ self.content = content
15
+ super().__init__(f"{self.status_code} - {self.content}")
16
+
17
+
18
+ class AirTableAPIError(AirTableHttpError):
19
+ def __init__(self, status_code: int, payload) -> None:
20
+ super().__init__(status_code, json.dumps(payload))
21
+ self.payload = payload
22
+ self.code = status_code
23
+ self.message = payload.get("error", {}).get("message", "")
24
+
25
+ def __str__(self) -> str:
26
+ return f"{self.code} - {self.message}"
27
+
28
+ def __repr__(self) -> str:
29
+ return str(self.payload)
30
+
31
+
32
+ def wrap_airtable_http_error(func):
33
+ @wraps(func)
34
+ def _wrapper(*args, **kwargs):
35
+ try:
36
+ return func(*args, **kwargs)
37
+ except HTTPError as e:
38
+ try:
39
+ raise AirTableAPIError(e.response.status_code, e.response.json())
40
+ except JSONDecodeError:
41
+ raise AirTableHttpError(
42
+ e.response.status_code, e.response.content.decode()
43
+ )
44
+
45
+ return _wrapper
@@ -0,0 +1,11 @@
1
+ EVENT_TYPES = "orders"
2
+ SECURITY_ALGORITHM = "HS256"
3
+ USER_AGENT = "swo-extensions/1.0"
4
+ CONSUME_EVENTS_HELP_TEXT = "Consume events from the MPT platform"
5
+ DEFAULT_APP_CONFIG_GROUP = "swo.mpt.ext"
6
+ DEFAULT_APP_CONFIG_NAME = "app_config"
7
+ DJANGO_SETTINGS_MODULE = "mpt_extension_sdk.runtime.djapp.conf.default"
8
+ ERR_DJANGO_SETTINGS_MODULE_TEXT = (
9
+ "DJANGO_SETTINGS_MODULE environment variable is not set. "
10
+ "Please set it to your Django settings module before running this command."
11
+ )
File without changes
File without changes
@@ -0,0 +1,16 @@
1
+ from collections.abc import Mapping, Sequence
2
+ from dataclasses import dataclass
3
+ from typing import Annotated, Literal
4
+
5
+ from typing_extensions import Doc
6
+
7
+ from mpt_extension_sdk.constants import EVENT_TYPES
8
+
9
+ EventType = Annotated[Literal[EVENT_TYPES], Doc("Unique identifier of the event type.")]
10
+
11
+
12
+ @dataclass
13
+ class Event:
14
+ id: Annotated[str, Doc("The unique identifier of the event.")]
15
+ type: EventType
16
+ data: Annotated[Mapping | Sequence, Doc("Event data.")]
@@ -0,0 +1,56 @@
1
+ from collections.abc import MutableMapping, Sequence
2
+ from typing import Any, Callable
3
+
4
+ from .dataclasses import Event, EventType
5
+
6
+ EventListener = Callable[[Any, Event], None]
7
+
8
+
9
+ class EventsRegistry:
10
+ def __init__(
11
+ self,
12
+ ) -> None:
13
+ self.listeners: MutableMapping[str, EventListener] = {}
14
+
15
+ def listener(
16
+ self,
17
+ event_type: EventType,
18
+ /,
19
+ ) -> Callable[[EventListener], EventListener]:
20
+ """
21
+ Unique identifier of the event type.
22
+
23
+ ## Example
24
+
25
+ ```python
26
+ from mpt_extension_sdk.core import Extension
27
+
28
+ ext = Extension()
29
+
30
+
31
+ @ext.events.listener("orders")
32
+ def process_order(client, event):
33
+ ...
34
+ ```
35
+ """
36
+
37
+ def decorator(func: EventListener) -> EventListener:
38
+ self.listeners[event_type] = func
39
+ return func
40
+
41
+ return decorator
42
+
43
+ def get_listener(
44
+ self,
45
+ event_type: EventType,
46
+ ) -> EventListener | None:
47
+ return self.listeners.get(event_type)
48
+
49
+ def get_registered_types(self) -> Sequence[str]:
50
+ return list(self.listeners.keys())
51
+
52
+ def is_event_supported(
53
+ self,
54
+ event_type: EventType,
55
+ ) -> bool:
56
+ return event_type in self.listeners
@@ -0,0 +1,12 @@
1
+ from ninja import NinjaAPI
2
+
3
+ from mpt_extension_sdk.core.events.registry import EventsRegistry
4
+
5
+
6
+ class Extension:
7
+ def __init__(
8
+ self,
9
+ /,
10
+ ) -> None:
11
+ self.events: EventsRegistry = EventsRegistry()
12
+ self.api: NinjaAPI = NinjaAPI()
@@ -0,0 +1,50 @@
1
+ import logging
2
+ from collections.abc import Mapping
3
+ from typing import Any, Callable
4
+
5
+ import jwt
6
+ from django.http import HttpRequest
7
+ from ninja.security import HttpBearer
8
+
9
+ from mpt_extension_sdk.constants import SECURITY_ALGORITHM
10
+ from mpt_extension_sdk.mpt_http.base import MPTClient
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class JWTAuth(HttpBearer):
16
+ JWT_ALGOS = [SECURITY_ALGORITHM]
17
+
18
+ def __init__(
19
+ self,
20
+ secret_callback: Callable[[MPTClient, Mapping[str, Any]], str],
21
+ ) -> None:
22
+ self.secret_callback = secret_callback
23
+ super().__init__()
24
+
25
+ def authenticate(self, request: HttpRequest, token: str) -> Any | None:
26
+ 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)}")
@@ -0,0 +1,17 @@
1
+ from django.conf import settings
2
+
3
+ from mpt_extension_sdk.mpt_http.base import MPTClient
4
+
5
+
6
+ def setup_client():
7
+ return MPTClient(
8
+ f"{settings.MPT_API_BASE_URL}/v1/",
9
+ settings.MPT_API_TOKEN,
10
+ )
11
+
12
+
13
+ def setup_operations_client():
14
+ return MPTClient(
15
+ f"{settings.MPT_API_BASE_URL}/v1/",
16
+ settings.MPT_API_TOKEN_OPERATIONS,
17
+ )
File without changes
@@ -0,0 +1,39 @@
1
+ from dataclasses import asdict, dataclass
2
+
3
+ ORDER_TYPE_PURCHASE = "Purchase"
4
+ ORDER_TYPE_CHANGE = "Change"
5
+ ORDER_TYPE_TERMINATION = "Termination"
6
+
7
+
8
+ @dataclass
9
+ class Context:
10
+ order: dict
11
+
12
+ @property
13
+ def order_id(self):
14
+ return self.order.get("id", None)
15
+
16
+ @property
17
+ def order_type(self):
18
+ return self.order.get("type", None)
19
+
20
+ @property
21
+ def product_id(self):
22
+ return self.order.get("product", {}).get("id", None)
23
+
24
+ def is_purchase_order(self):
25
+ return self.order["type"] == ORDER_TYPE_PURCHASE
26
+
27
+ def is_change_order(self):
28
+ return self.order["type"] == ORDER_TYPE_CHANGE
29
+
30
+ def is_termination_order(self):
31
+ return self.order["type"] == ORDER_TYPE_TERMINATION
32
+
33
+ @classmethod
34
+ def from_context(cls, context):
35
+ base_data = asdict(context)
36
+ return cls(**base_data)
37
+
38
+ def __str__(self):
39
+ return f"Context: {self.order.get("id", None)} {self.order.get("type", None)}"
@@ -0,0 +1,51 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Callable
3
+
4
+ from mpt_extension_sdk.flows.context import Context
5
+ from mpt_extension_sdk.mpt_http.base import MPTClient
6
+
7
+ NextStep = Callable[[MPTClient, Context], None]
8
+
9
+
10
+ class Step(ABC):
11
+ @abstractmethod
12
+ def __call__(
13
+ self,
14
+ client: MPTClient,
15
+ context: Context,
16
+ next_step: NextStep,
17
+ ) -> None:
18
+ raise NotImplementedError() # pragma: no cover
19
+
20
+
21
+ def _default_error_handler(error: Exception, context: Context, next_step: NextStep):
22
+ raise error
23
+
24
+
25
+ class Cursor:
26
+ def __init__(self, steps, error_handler):
27
+ self.queue = steps
28
+ self.error_handler = error_handler
29
+
30
+ def __call__(self, client: MPTClient, context: Context):
31
+ if not self.queue:
32
+ return
33
+ current_step = self.queue[0]
34
+ next_step = Cursor(self.queue[1:], self.error_handler)
35
+
36
+ try:
37
+ current_step(client, context, next_step)
38
+ except Exception as error:
39
+ self.error_handler(error, context, next_step)
40
+
41
+
42
+ class Pipeline:
43
+ def __init__(self, *steps):
44
+ self.queue = steps
45
+
46
+ def run(self, client: MPTClient, context: Context, error_handler=None):
47
+ execute = Cursor(self.queue, error_handler or _default_error_handler)
48
+ return execute(client, context)
49
+
50
+ def __len__(self):
51
+ return len(self.queue)
File without changes
@@ -0,0 +1,110 @@
1
+ import logging
2
+
3
+ from azure.core.exceptions import (
4
+ HttpResponseError,
5
+ ResourceNotFoundError,
6
+ )
7
+ from azure.identity import DefaultAzureCredential
8
+ from azure.keyvault.secrets import SecretClient
9
+ from requests import Session
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class KeyVault(Session):
15
+ def __init__(self, key_vault_name: str):
16
+ """
17
+ Initialize the KeyVault client with the provided Key Vault name.
18
+
19
+ Args:
20
+ key_vault_name (str): The name of the Azure Key Vault.
21
+ """
22
+ super().__init__()
23
+ self.key_vault_name = key_vault_name
24
+
25
+ def get_secret(self, secret_name: str):
26
+ """
27
+ Retrieve a secret from the Azure Key Vault.
28
+
29
+ Args:
30
+ key_vault_url (str): The URL of the Azure Key Vault.
31
+ secret_name (str): The name of the secret to retrieve.
32
+
33
+ Returns:
34
+ str: The value of the secret.
35
+ """
36
+ 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}"
46
+ )
47
+ return None
48
+
49
+ def set_secret(self, secret_name: str, secret_value: str):
50
+ """
51
+ Set a secret in the Azure Key Vault.
52
+
53
+ Args:
54
+ key_vault_url (str): The URL of the Azure Key Vault.
55
+ secret_name (str): The name of the secret to set.
56
+ secret_value (str): The value of the secret to set.
57
+
58
+ Returns:
59
+ None
60
+ """
61
+
62
+ # Set the secret in the Key Vault
63
+ try:
64
+ client = self._get_key_vault_client(self.key_vault_name)
65
+
66
+ client.set_secret(secret_name, secret_value)
67
+
68
+ updated_secret = client.get_secret(secret_name)
69
+
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
76
+
77
+ def _get_key_vault_url(self, key_vault_name: str): # pragma: no cover
78
+ """
79
+ Construct the Key Vault URL using the provided Key Vault name.
80
+
81
+ Args:
82
+ key_vault_name (str): The name of the Azure Key Vault.
83
+
84
+ Returns:
85
+ str: The URL of the Azure Key Vault.
86
+ """
87
+
88
+ # Construct the Key Vault URL
89
+ return f"https://{key_vault_name}.vault.azure.net/"
90
+
91
+ def _get_key_vault_client(self, key_vault_name: str): # pragma: no cover
92
+ """
93
+ Create a Key Vault client using the provided Key Vault URL and secret name.
94
+
95
+ Args:
96
+ key_vault_url (str): The URL of the Azure Key Vault.
97
+
98
+ Returns:
99
+ SecretClient: An instance of the SecretClient for the specified Key Vault.
100
+ """
101
+
102
+ # Get the Key Vault URL
103
+ key_vault_url = self._get_key_vault_url(key_vault_name)
104
+ # Create a credential object using DefaultAzureCredential
105
+ credential = DefaultAzureCredential()
106
+
107
+ # Create a Key Vault client using the credential
108
+ client = SecretClient(vault_url=key_vault_url, credential=credential)
109
+
110
+ return client
File without changes
@@ -0,0 +1,43 @@
1
+ from urllib.parse import urljoin
2
+
3
+ from requests import Session
4
+ from requests.adapters import HTTPAdapter, Retry
5
+
6
+ from mpt_extension_sdk.constants import USER_AGENT
7
+
8
+
9
+ class MPTClient(Session):
10
+ def __init__(self, base_url, api_token):
11
+ super().__init__()
12
+ retries = Retry(
13
+ total=5,
14
+ backoff_factor=0.1,
15
+ status_forcelist=[500, 502, 503, 504],
16
+ )
17
+ self.mount(
18
+ "http://",
19
+ HTTPAdapter(
20
+ max_retries=retries,
21
+ pool_maxsize=36,
22
+ ),
23
+ )
24
+ self.headers.update(
25
+ {
26
+ "User-Agent": USER_AGENT,
27
+ "Authorization": f"Bearer {api_token}",
28
+ },
29
+ )
30
+ self.base_url = f"{base_url}/" if base_url[-1] != "/" else base_url
31
+ self.api_token = api_token
32
+
33
+ def request(self, method, url, *args, **kwargs):
34
+ url = self.join_url(url)
35
+ return super().request(method, url, *args, **kwargs)
36
+
37
+ def prepare_request(self, request, *args, **kwargs):
38
+ request.url = self.join_url(request.url)
39
+ return super().prepare_request(request, *args, **kwargs)
40
+
41
+ def join_url(self, url):
42
+ url = url[1:] if url[0] == "/" else url
43
+ return urljoin(self.base_url, url)