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.
- mpt_extension_sdk/airtable/wrap_http_error.py +10 -6
- mpt_extension_sdk/constants.py +2 -0
- mpt_extension_sdk/core/events/dataclasses.py +2 -0
- mpt_extension_sdk/core/events/registry.py +8 -3
- mpt_extension_sdk/core/extension.py +2 -0
- mpt_extension_sdk/core/security.py +35 -27
- mpt_extension_sdk/core/utils.py +2 -0
- mpt_extension_sdk/flows/context.py +12 -1
- mpt_extension_sdk/flows/pipeline.py +11 -2
- mpt_extension_sdk/key_vault/base.py +36 -25
- mpt_extension_sdk/mpt_http/base.py +15 -3
- mpt_extension_sdk/mpt_http/mpt.py +73 -12
- mpt_extension_sdk/mpt_http/utils.py +1 -0
- mpt_extension_sdk/mpt_http/wrap_http_error.py +30 -8
- mpt_extension_sdk/runtime/__init__.py +1 -0
- mpt_extension_sdk/runtime/commands/django.py +5 -5
- mpt_extension_sdk/runtime/commands/run.py +2 -4
- mpt_extension_sdk/runtime/djapp/apps.py +7 -1
- mpt_extension_sdk/runtime/djapp/conf/__init__.py +2 -3
- mpt_extension_sdk/runtime/djapp/conf/default.py +9 -15
- mpt_extension_sdk/runtime/djapp/management/commands/consume_events.py +12 -7
- mpt_extension_sdk/runtime/djapp/middleware.py +5 -3
- mpt_extension_sdk/runtime/errors.py +5 -0
- mpt_extension_sdk/runtime/events/dispatcher.py +21 -19
- mpt_extension_sdk/runtime/events/producers.py +29 -25
- mpt_extension_sdk/runtime/events/utils.py +6 -5
- mpt_extension_sdk/runtime/initializer.py +4 -5
- mpt_extension_sdk/runtime/logging.py +11 -2
- mpt_extension_sdk/runtime/master.py +25 -17
- mpt_extension_sdk/runtime/swoext.py +10 -8
- mpt_extension_sdk/runtime/tracer.py +2 -0
- mpt_extension_sdk/runtime/utils.py +37 -38
- mpt_extension_sdk/runtime/workers.py +14 -8
- mpt_extension_sdk/swo_rql/query_builder.py +17 -14
- mpt_extension_sdk-5.17.2.dist-info/METADATA +39 -0
- mpt_extension_sdk-5.17.2.dist-info/RECORD +54 -0
- {mpt_extension_sdk-4.5.0.dist-info → mpt_extension_sdk-5.17.2.dist-info}/WHEEL +1 -1
- mpt_extension_sdk-5.17.2.dist-info/entry_points.txt +5 -0
- mpt_extension_sdk-4.5.0.dist-info/METADATA +0 -45
- mpt_extension_sdk-4.5.0.dist-info/RECORD +0 -53
- mpt_extension_sdk-4.5.0.dist-info/entry_points.txt +0 -6
- {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
|
-
|
|
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
|
|
43
|
+
except HTTPError as err:
|
|
38
44
|
try:
|
|
39
|
-
raise AirTableAPIError(
|
|
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
|
mpt_extension_sdk/constants.py
CHANGED
|
@@ -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
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from collections.abc import Mapping
|
|
3
|
-
from typing import Any,
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
mpt_extension_sdk/core/utils.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
+
def _get_secret_from_key_vault(self, secret_name: str):
|
|
71
|
+
"""
|
|
72
|
+
Retrieve a secret from the Azure Key Vault.
|
|
67
73
|
|
|
68
|
-
|
|
74
|
+
Args:
|
|
75
|
+
secret_name (str): The name of the secret to retrieve.
|
|
69
76
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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 =
|
|
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)
|