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.
- mpt_extension_sdk/__init__.py +0 -0
- mpt_extension_sdk/airtable/__init__.py +0 -0
- mpt_extension_sdk/airtable/wrap_http_error.py +45 -0
- mpt_extension_sdk/constants.py +11 -0
- mpt_extension_sdk/core/__init__.py +0 -0
- mpt_extension_sdk/core/events/__init__.py +0 -0
- mpt_extension_sdk/core/events/dataclasses.py +16 -0
- mpt_extension_sdk/core/events/registry.py +56 -0
- mpt_extension_sdk/core/extension.py +12 -0
- mpt_extension_sdk/core/security.py +50 -0
- mpt_extension_sdk/core/utils.py +17 -0
- mpt_extension_sdk/flows/__init__.py +0 -0
- mpt_extension_sdk/flows/context.py +39 -0
- mpt_extension_sdk/flows/pipeline.py +51 -0
- mpt_extension_sdk/key_vault/__init__.py +0 -0
- mpt_extension_sdk/key_vault/base.py +110 -0
- mpt_extension_sdk/mpt_http/__init__.py +0 -0
- mpt_extension_sdk/mpt_http/base.py +43 -0
- mpt_extension_sdk/mpt_http/mpt.py +530 -0
- mpt_extension_sdk/mpt_http/utils.py +2 -0
- mpt_extension_sdk/mpt_http/wrap_http_error.py +68 -0
- mpt_extension_sdk/runtime/__init__.py +10 -0
- mpt_extension_sdk/runtime/commands/__init__.py +0 -0
- mpt_extension_sdk/runtime/commands/django.py +42 -0
- mpt_extension_sdk/runtime/commands/run.py +44 -0
- mpt_extension_sdk/runtime/djapp/__init__.py +0 -0
- mpt_extension_sdk/runtime/djapp/apps.py +46 -0
- mpt_extension_sdk/runtime/djapp/conf/__init__.py +12 -0
- mpt_extension_sdk/runtime/djapp/conf/default.py +225 -0
- mpt_extension_sdk/runtime/djapp/conf/urls.py +9 -0
- mpt_extension_sdk/runtime/djapp/management/__init__.py +0 -0
- mpt_extension_sdk/runtime/djapp/management/commands/__init__.py +0 -0
- mpt_extension_sdk/runtime/djapp/management/commands/consume_events.py +38 -0
- mpt_extension_sdk/runtime/djapp/middleware.py +21 -0
- mpt_extension_sdk/runtime/events/__init__.py +0 -0
- mpt_extension_sdk/runtime/events/dispatcher.py +83 -0
- mpt_extension_sdk/runtime/events/producers.py +108 -0
- mpt_extension_sdk/runtime/events/utils.py +85 -0
- mpt_extension_sdk/runtime/initializer.py +62 -0
- mpt_extension_sdk/runtime/logging.py +36 -0
- mpt_extension_sdk/runtime/master.py +136 -0
- mpt_extension_sdk/runtime/swoext.py +69 -0
- mpt_extension_sdk/runtime/tracer.py +18 -0
- mpt_extension_sdk/runtime/utils.py +148 -0
- mpt_extension_sdk/runtime/workers.py +90 -0
- mpt_extension_sdk/swo_rql/__init__.py +5 -0
- mpt_extension_sdk/swo_rql/constants.py +7 -0
- mpt_extension_sdk/swo_rql/query_builder.py +392 -0
- mpt_extension_sdk-4.5.0.dist-info/METADATA +45 -0
- mpt_extension_sdk-4.5.0.dist-info/RECORD +53 -0
- mpt_extension_sdk-4.5.0.dist-info/WHEEL +4 -0
- mpt_extension_sdk-4.5.0.dist-info/entry_points.txt +6 -0
- 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,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)
|