mpt-extension-sdk 5.6.0__tar.gz → 5.7.0__tar.gz

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