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
@@ -19,15 +19,18 @@ logger = logging.getLogger(__name__)
19
19
 
20
20
 
21
21
  def done_callback(futures, key, future): # pragma: no cover
22
+ """Callback for when a future is done."""
22
23
  del futures[key]
23
24
  exc = future.exception()
24
25
  if not exc:
25
- logger.debug(f"Future for {key} has been completed successfully")
26
+ logger.debug("Future for %s has been completed successfully", key)
26
27
  return
27
- logger.error(f"Future for {key} has failed: {exc}")
28
+ logger.error("Future for %s has failed: %s", key, exc)
28
29
 
29
30
 
30
31
  class Dispatcher:
32
+ """Event dispatcher."""
33
+
31
34
  def __init__(self, group=DEFAULT_APP_CONFIG_GROUP, name=DEFAULT_APP_CONFIG_NAME):
32
35
  self.registry: EventsRegistry = get_events_registry(group=group, name=name)
33
36
  self.queue = deque()
@@ -38,46 +41,45 @@ class Dispatcher:
38
41
  self.client = setup_client()
39
42
 
40
43
  def start(self):
44
+ """Start the dispatcher."""
41
45
  self.running_event.set()
42
46
  self.processor.start()
43
47
 
44
48
  def stop(self):
49
+ """Stop the dispatcher."""
45
50
  self.running_event.clear()
46
51
  self.processor.join()
47
52
 
48
53
  @property
49
54
  def running(self):
55
+ """Return True if the dispatcher is running."""
50
56
  return self.running_event.is_set()
51
57
 
52
58
  def dispatch_event(self, event: Event): # pragma: no cover
59
+ """Dispatch an event to the appropriate listener."""
53
60
  if self.registry.is_event_supported(event.type):
54
- logger.info(f"event of type {event.type} with id {event.id} accepted")
61
+ logger.info("event of type %s with id %s accepted", event.type, event.id)
55
62
  self.queue.appendleft((event.type, event))
56
63
 
57
64
  def process_events(self): # pragma: no cover
65
+ """Process events from the queue."""
58
66
  while self.running:
59
67
  skipped = []
60
68
  while len(self.queue) > 0:
61
69
  event_type, event = self.queue.pop()
62
- logger.debug(
63
- f"got event of type {event_type} ({event.id}) from queue..."
64
- )
65
- listener = wrap_for_trace(
66
- self.registry.get_listener(event_type), event_type
67
- )
68
- if (event.type, event.id) not in self.futures:
69
- future = self.executor.submit(listener, self.client, event)
70
- self.futures[(event.type, event.id)] = future
71
- future.add_done_callback(
72
- functools.partial(
73
- done_callback, self.futures, (event.type, event.id)
74
- )
75
- )
76
- else:
70
+ logger.debug("got event of type %s (%s) from queue...", event_type, event.id)
71
+ listener = wrap_for_trace(self.registry.get_listener(event_type), event_type)
72
+ if (event.type, event.id) in self.futures:
77
73
  logger.info(
78
- f"An event for {(event.type, event.id)} is already processing, skip it"
74
+ "An event for (%s, %s) is already processing, skip it", event.type, event.id
79
75
  )
80
76
  skipped.append((event.type, event))
77
+ else:
78
+ future = self.executor.submit(listener, self.client, event)
79
+ self.futures[event.type, event.id] = future
80
+ future.add_done_callback(
81
+ functools.partial(done_callback, self.futures, (event.type, event.id))
82
+ )
81
83
 
82
84
  self.queue.extendleft(skipped)
83
85
  time.sleep(0.5)
@@ -3,17 +3,22 @@ import threading
3
3
  import time
4
4
  from abc import ABC, abstractmethod
5
5
  from contextlib import contextmanager
6
+ from http import HTTPStatus
6
7
 
7
8
  import requests
8
9
  from django.conf import settings
10
+ from django.utils.module_loading import import_string
9
11
 
10
12
  from mpt_extension_sdk.core.events.dataclasses import Event
11
13
  from mpt_extension_sdk.core.utils import setup_client
14
+ from mpt_extension_sdk.swo_rql import RQLQuery
12
15
 
13
16
  logger = logging.getLogger(__name__)
14
17
 
15
18
 
16
19
  class EventProducer(ABC):
20
+ """Abstract base class for event producers."""
21
+
17
22
  def __init__(self, dispatcher):
18
23
  self.dispatcher = dispatcher
19
24
  self.running_event = threading.Event()
@@ -21,18 +26,22 @@ class EventProducer(ABC):
21
26
 
22
27
  @property
23
28
  def running(self):
29
+ """Return True if the producer is running."""
24
30
  return self.running_event.is_set()
25
31
 
26
32
  def start(self):
33
+ """Start the event producer."""
27
34
  self.running_event.set()
28
35
  self.producer.start()
29
36
 
30
37
  def stop(self):
38
+ """Stop the event producer."""
31
39
  self.running_event.clear()
32
40
  self.producer.join()
33
41
 
34
42
  @contextmanager
35
43
  def sleep(self, secs, interval=0.5): # pragma: no cover
44
+ """Sleep for a given number of seconds."""
36
45
  yield
37
46
  sleeped = 0
38
47
  while sleeped < secs and self.running_event.is_set():
@@ -41,44 +50,40 @@ class EventProducer(ABC):
41
50
 
42
51
  @abstractmethod
43
52
  def produce_events(self):
44
- pass
45
-
46
- @abstractmethod
47
- def produce_events_with_context(self):
48
- pass
53
+ """Produce events."""
49
54
 
50
55
 
51
56
  class OrderEventProducer(EventProducer):
57
+ """Order event producer."""
58
+
52
59
  def __init__(self, dispatcher):
53
60
  super().__init__(dispatcher)
54
61
  self.client = setup_client()
62
+ self.setup_contexts = import_string(settings.MPT_SETUP_CONTEXTS_FUNC)
55
63
 
56
64
  def produce_events(self):
65
+ """Produce order events."""
57
66
  while self.running:
58
67
  with self.sleep(settings.MPT_ORDERS_API_POLLING_INTERVAL_SECS):
59
68
  orders = self.get_processing_orders()
60
- logger.info(f"{len(orders)} orders found for processing...")
61
- for order in orders:
62
- self.dispatcher.dispatch_event(Event(order["id"], "orders", order))
69
+ logger.info("%d orders found for processing...", len(orders))
70
+ self.dispatch_events(orders)
63
71
 
64
- def produce_events_with_context(self): # pragma: no cover
65
- while self.running:
66
- with self.sleep(settings.MPT_ORDERS_API_POLLING_INTERVAL_SECS):
67
- orders = self.get_processing_orders()
68
- orders, contexts = self.filter_and_enrich(self.client, orders)
69
- logger.info(f"{len(orders)} orders found for processing...")
70
- for order, context in zip(orders, contexts):
71
- self.dispatcher.dispatch_event(
72
- Event(order["id"], "orders", order, context)
73
- )
72
+ def dispatch_events(self, orders):
73
+ """Dispatch events for the given orders."""
74
+ contexts = self.setup_contexts(self.client, orders)
75
+ for context in contexts:
76
+ self.dispatcher.dispatch_event(Event(context.order_id, "orders", context))
74
77
 
75
78
  def get_processing_orders(self):
76
- products = ",".join(settings.MPT_PRODUCTS_IDS)
79
+ """Get processing orders."""
77
80
  orders = []
78
- rql_query = f"and(in(agreement.product.id,({products})),eq(status,processing))"
81
+ rql_query = RQLQuery().agreement.product.id.in_(settings.MPT_PRODUCTS_IDS) & RQLQuery(
82
+ status="processing"
83
+ )
79
84
  url = (
80
85
  f"/commerce/orders?{rql_query}&select=audit,parameters,lines,subscriptions,"
81
- f"subscriptions.lines,agreement,buyer,seller&order=audit.created.at"
86
+ f"subscriptions.lines,agreement,buyer,seller,authorization.externalIds&order=audit.created.at"
82
87
  )
83
88
  page = None
84
89
  limit = 10
@@ -89,19 +94,18 @@ class OrderEventProducer(EventProducer):
89
94
  except requests.RequestException:
90
95
  logger.exception("Cannot retrieve orders")
91
96
  return []
92
- if response.status_code == 200:
97
+ if response.status_code == HTTPStatus.OK.value:
93
98
  page = response.json()
94
99
  orders.extend(page["data"])
95
100
  else:
96
- logger.warning(
97
- f"Order API error: {response.status_code} {response.content}"
98
- )
101
+ logger.warning("Order API error: %s %s", response.status_code, response.content)
99
102
  return []
100
103
  offset += limit
101
104
 
102
105
  return orders
103
106
 
104
107
  def has_more_pages(self, orders):
108
+ """Check if there are more pages of orders."""
105
109
  if not orders:
106
110
  return True
107
111
  pagination = orders["$meta"]["pagination"]
@@ -32,6 +32,7 @@ def _response_hook(span, request, response): # pragma: no cover
32
32
 
33
33
 
34
34
  def instrument_logging(): # pragma: no cover
35
+ """Instrument logging for OpenTelemetry."""
35
36
  exporter = AzureMonitorTraceExporter(
36
37
  connection_string=settings.APPLICATIONINSIGHTS_CONNECTION_STRING
37
38
  )
@@ -46,14 +47,14 @@ def instrument_logging(): # pragma: no cover
46
47
 
47
48
 
48
49
  def wrap_for_trace(func, event_type): # pragma: no cover
50
+ """Wrap a function to add OpenTelemetry tracing."""
51
+
49
52
  @wraps(func)
50
53
  def opentelemetry_wrapper(client, event):
51
54
  tracer = trace.get_tracer(event_type)
52
55
  object_id = event.id
53
56
 
54
- with tracer.start_as_current_span(
55
- f"Event {event_type} for {object_id}"
56
- ) as span:
57
+ with tracer.start_as_current_span(f"Event {event_type} for {object_id}") as span:
57
58
  try:
58
59
  func(client, event)
59
60
  except Exception:
@@ -74,12 +75,12 @@ def wrap_for_trace(func, event_type): # pragma: no cover
74
75
 
75
76
  def setup_contexts(mpt_client, orders):
76
77
  """
77
- List of contexts from orders
78
+ List of contexts from orders.
79
+
78
80
  Args:
79
81
  mpt_client (MPTClient): MPT client
80
82
  orders (list): List of orders
81
83
 
82
84
  Returns: List of contexts
83
-
84
85
  """
85
86
  return [Context(order=order) for order in orders]
@@ -25,13 +25,12 @@ JSON_EXT_VARIABLES = {
25
25
 
26
26
 
27
27
  def initialize(options, group=DEFAULT_APP_CONFIG_GROUP, name=DEFAULT_APP_CONFIG_NAME):
28
+ """Initialize the SDK."""
28
29
  rich.reconfigure(theme=Theme({"repr.mpt_id": "bold light_salmon3"}))
29
- django_settings_module = options.get(
30
- "django_settings_module", DJANGO_SETTINGS_MODULE
31
- )
30
+ django_settings_module = options.get("django_settings_module", DJANGO_SETTINGS_MODULE)
32
31
  os.environ.setdefault("DJANGO_SETTINGS_MODULE", django_settings_module)
33
- import django
34
- from django.conf import settings
32
+ import django # noqa: PLC0415
33
+ from django.conf import settings # noqa: PLC0415
35
34
 
36
35
  root_logging_handler = "rich" if options.get("color") else "console"
37
36
  if settings.USE_APPLICATIONINSIGHTS:
@@ -1,8 +1,12 @@
1
+ from typing import ClassVar
2
+
1
3
  from rich.highlighter import ReprHighlighter as _ReprHighlighter
2
4
  from rich.logging import RichHandler as _RichHandler
3
5
 
4
6
 
5
7
  class ReprHighlighter(_ReprHighlighter):
8
+ """Highlighter for MPT IDs."""
9
+
6
10
  accounts_prefixes = ("ACC", "BUY", "LCE", "MOD", "SEL", "USR", "AUSR", "UGR")
7
11
  catalog_prefixes = (
8
12
  "PRD",
@@ -27,10 +31,15 @@ class ReprHighlighter(_ReprHighlighter):
27
31
  *commerce_prefixes,
28
32
  *aux_prefixes,
29
33
  )
30
- highlights = _ReprHighlighter.highlights + [
31
- rf"(?P<mpt_id>(?:{'|'.join(all_prefixes)})(?:-\d{{4}})*)"
34
+ prefixes_pattern = "|".join(all_prefixes)
35
+ pattern = rf"(?P<mpt_id>(?:{prefixes_pattern})(?:-\d{{4}})*)"
36
+ highlights: ClassVar[list[str]] = [
37
+ *_ReprHighlighter.highlights,
38
+ pattern,
32
39
  ]
33
40
 
34
41
 
35
42
  class RichHandler(_RichHandler):
43
+ """Rich handler for logging with color support."""
44
+
36
45
  HIGHLIGHTER_CLASS = ReprHighlighter
@@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
15
15
 
16
16
 
17
17
  HANDLED_SIGNALS = (signal.SIGINT, signal.SIGTERM)
18
- PROCESS_CHECK_INTERVAL_SECS = int(os.environ.get("PROCESS_CHECK_INTERVAL_SECS", 5))
18
+ PROCESS_CHECK_INTERVAL_SECS = int(os.environ.get("PROCESS_CHECK_INTERVAL_SECS", "5"))
19
19
 
20
20
 
21
21
  def _display_path(path): # pragma: no cover
@@ -26,6 +26,8 @@ def _display_path(path): # pragma: no cover
26
26
 
27
27
 
28
28
  class Master:
29
+ """Master process for managing worker processes."""
30
+
29
31
  def __init__(self, options, settings):
30
32
  self.workers = {}
31
33
  self.options = options
@@ -61,13 +63,16 @@ class Master:
61
63
  }
62
64
 
63
65
  def setup_signals_handler(self):
66
+ """Setup signal handlers for termination signals."""
64
67
  for sig in HANDLED_SIGNALS:
65
68
  signal.signal(sig, self.handle_signal)
66
69
 
67
70
  def handle_signal(self, *args, **kwargs):
71
+ """Handle termination signals."""
68
72
  self.stop_event.set()
69
73
 
70
74
  def start(self):
75
+ """Start all worker processes."""
71
76
  for worker_type, target in self.proc_targets.items():
72
77
  self.start_worker_process(worker_type, target)
73
78
  self.monitor_thread = threading.Thread(target=self.monitor_processes)
@@ -75,40 +80,42 @@ class Master:
75
80
  self.monitor_thread.start()
76
81
 
77
82
  def start_worker_process(self, worker_type, target):
78
- p = start_process(target, "function", (self.options,), {})
79
- self.workers[worker_type] = p
80
- logger.info(f"{worker_type.capitalize()} worker pid: {p.pid}")
83
+ """Start a worker process."""
84
+ worker_proc = start_process(target, "function", (self.options,), {})
85
+ self.workers[worker_type] = worker_proc
86
+ logger.info("%s worker pid: %s", worker_type.capitalize(), worker_proc.pid)
81
87
 
82
88
  def monitor_processes(self): # pragma: no cover
89
+ """Monitor the status of worker processes."""
83
90
  while self.monitor_event.is_set():
84
91
  exited_workers = []
85
- for worker_type, p in self.workers.items():
86
- if not p.is_alive():
87
- if p.exitcode != 0:
88
- logger.info(
89
- f"Process of type {worker_type} is dead, restart it"
90
- )
91
- self.start_worker_process(
92
- worker_type, self.proc_targets[worker_type]
93
- )
94
- else:
92
+ for worker_type, worker_proc in self.workers.items():
93
+ if not worker_proc.is_alive():
94
+ if worker_proc.exitcode == 0:
95
95
  exited_workers.append(worker_type)
96
- logger.info(f"{worker_type.capitalize()} worker exited")
96
+ logger.info("%s worker exited", worker_type.capitalize())
97
+ else:
98
+ logger.info("Process of type %s is dead, restart it", worker_type)
99
+ self.start_worker_process(worker_type, self.proc_targets[worker_type])
97
100
  if exited_workers == list(self.workers.keys()):
98
101
  self.stop_event.set()
99
102
 
100
103
  time.sleep(PROCESS_CHECK_INTERVAL_SECS)
101
104
 
102
105
  def stop(self):
106
+ """Stop all worker processes."""
103
107
  self.monitor_event.clear()
104
108
  self.monitor_thread.join()
105
109
  for worker_type, process in self.workers.items():
106
110
  process.stop(sigint_timeout=5, sigkill_timeout=1)
107
111
  logger.info(
108
- f"{worker_type.capitalize()} process with pid {process.pid} stopped."
112
+ "%s process with pid %s stopped.",
113
+ worker_type.capitalize(),
114
+ process.pid,
109
115
  )
110
116
 
111
117
  def restart(self):
118
+ """Restart the master process."""
112
119
  self.stop()
113
120
  self.start()
114
121
 
@@ -118,10 +125,11 @@ class Master:
118
125
  def __next__(self): # pragma: no cover
119
126
  changes = next(self.watcher)
120
127
  if changes:
121
- return list({Path(c[1]) for c in changes})
128
+ return list({Path(change[1]) for change in changes})
122
129
  return None
123
130
 
124
131
  def run(self): # pragma: no cover
132
+ """Run the master process."""
125
133
  self.start()
126
134
  if self.options.get("reload"):
127
135
  for files_changed in self:
@@ -6,6 +6,7 @@ from mpt_extension_sdk.runtime.utils import show_banner
6
6
 
7
7
 
8
8
  def print_version(ctx, param, value):
9
+ """Print the version of the SoftwareOne Extension CLI."""
9
10
  if not value or ctx.resilient_parsing:
10
11
  return
11
12
  click.echo(f"SoftwareOne Extension CLI, version {get_version()}")
@@ -22,7 +23,7 @@ def print_version(ctx, param, value):
22
23
  )
23
24
  @click.pass_context
24
25
  def cli(ctx):
25
- """SoftwareOne Extension CLI"""
26
+ """SoftwareOne Extension CLI."""
26
27
  show_banner()
27
28
 
28
29
 
@@ -36,32 +37,33 @@ for cmd in map(
36
37
  cli.add_command(cmd)
37
38
 
38
39
 
39
- def make_django_command(name, django_command=None, help=None):
40
- "A wrapper to convert a Django subcommand a Click command"
40
+ def make_django_command(name, django_command=None, help_value=None):
41
+ """A wrapper to convert a Django subcommand a Click command."""
41
42
  if django_command is None:
42
43
  django_command = name
43
44
 
44
45
  @click.command(
45
46
  name=name,
46
- help=help,
47
+ help=help_value,
47
48
  add_help_option=False,
48
- context_settings=dict(ignore_unknown_options=True),
49
+ context_settings={"ignore_unknown_options": True},
49
50
  )
50
51
  @click.argument("management_args", nargs=-1, type=click.UNPROCESSED)
51
52
  @click.pass_context
52
53
  def inner(ctx, management_args):
53
- from mpt_extension_sdk.runtime.commands.django import django
54
+ from mpt_extension_sdk.runtime.commands.django import django # noqa: PLC0415
54
55
 
55
- ctx.params["management_args"] = (django_command,) + management_args
56
+ ctx.params["management_args"] = (django_command, *management_args)
56
57
  ctx.forward(django)
57
58
 
58
59
  return inner
59
60
 
60
61
 
61
- cli.add_command(make_django_command("shell", help="Open Django console"))
62
+ cli.add_command(make_django_command("shell", help_value="Open Django console"))
62
63
 
63
64
 
64
65
  def main():
66
+ """Main entry point for the CLI."""
65
67
  cli(standalone_mode=False)
66
68
 
67
69
 
@@ -6,6 +6,8 @@ tracer = trace.get_tracer(__name__)
6
6
 
7
7
 
8
8
  def dynamic_trace_span(name_fn):
9
+ """Dynamically create a trace span."""
10
+
9
11
  def decorator(func):
10
12
  @wraps(func)
11
13
  def wrapper(*args, **kwargs):
@@ -2,6 +2,7 @@ import json
2
2
  import os
3
3
  import sys
4
4
  from importlib.metadata import entry_points
5
+ from pathlib import Path
5
6
 
6
7
  from django.apps import apps
7
8
  from django.contrib import admin
@@ -14,71 +15,72 @@ from rich.text import Text
14
15
  from mpt_extension_sdk.constants import (
15
16
  DEFAULT_APP_CONFIG_GROUP,
16
17
  DEFAULT_APP_CONFIG_NAME,
18
+ GRADIENT_HEX_BASE,
17
19
  )
20
+ from mpt_extension_sdk.runtime.errors import VariableNotWellFormedError
18
21
 
19
22
 
20
- def get_extension_app_config_name(
21
- group=DEFAULT_APP_CONFIG_GROUP, name=DEFAULT_APP_CONFIG_NAME
22
- ):
23
+ def get_extension_app_config_name(group=DEFAULT_APP_CONFIG_GROUP, name=DEFAULT_APP_CONFIG_NAME):
24
+ """Get the extension app config name for the specified group and name."""
23
25
  eps = entry_points()
24
26
  (app_config_ep,) = eps.select(group=group, name=name)
25
27
  app_config = app_config_ep.load()
26
28
  return f"{app_config.__module__}.{app_config.__name__}"
27
29
 
28
30
 
29
- def get_extension_app_config(
30
- group=DEFAULT_APP_CONFIG_GROUP, name=DEFAULT_APP_CONFIG_NAME
31
- ):
31
+ def get_extension_app_config(group=DEFAULT_APP_CONFIG_GROUP, name=DEFAULT_APP_CONFIG_NAME):
32
+ """Get the extension app config for the specified group and name."""
32
33
  app_config_name = get_extension_app_config_name(group=group, name=name)
33
34
  return next(
34
35
  filter(
35
- lambda app: app_config_name
36
- == f"{app.__class__.__module__}.{app.__class__.__name__}",
36
+ lambda app: app_config_name == get_app_name(app),
37
37
  apps.app_configs.values(),
38
38
  ),
39
39
  None,
40
40
  )
41
41
 
42
42
 
43
+ def get_app_name(app):
44
+ """Get the app name for the specified app."""
45
+ return f"{app.__class__.__module__}.{app.__class__.__name__}"
46
+
47
+
43
48
  def get_extension(group=DEFAULT_APP_CONFIG_GROUP, name=DEFAULT_APP_CONFIG_NAME):
49
+ """Get the extension for the specified group and name."""
44
50
  return get_extension_app_config(group=group, name=name).extension
45
51
 
46
52
 
47
53
  def get_events_registry(group=DEFAULT_APP_CONFIG_GROUP, name=DEFAULT_APP_CONFIG_NAME):
54
+ """Get the events registry for the extension."""
48
55
  return get_extension(group=group, name=name).events
49
56
 
50
57
 
51
58
  def gradient(start_hex, end_hex, num_samples=10): # pragma: no cover
52
- start_rgb = tuple(int(start_hex[i : i + 2], 16) for i in range(1, 6, 2))
53
- end_rgb = tuple(int(end_hex[i : i + 2], 16) for i in range(1, 6, 2))
59
+ """Retrieve the gradient."""
60
+ start_rgb = tuple(int(start_hex[idx : idx + 2], GRADIENT_HEX_BASE) for idx in range(1, 6, 2))
61
+ end_rgb = tuple(int(end_hex[idx : idx + 2], GRADIENT_HEX_BASE) for idx in range(1, 6, 2))
54
62
  gradient_colors = [start_hex]
55
63
  for sample in range(1, num_samples):
56
- red = int(
57
- start_rgb[0]
58
- + (float(sample) / (num_samples - 1)) * (end_rgb[0] - start_rgb[0])
59
- )
64
+ red = int(start_rgb[0] + (float(sample) / (num_samples - 1)) * (end_rgb[0] - start_rgb[0]))
60
65
  green = int(
61
- start_rgb[1]
62
- + (float(sample) / (num_samples - 1)) * (end_rgb[1] - start_rgb[1])
63
- )
64
- blue = int(
65
- start_rgb[2]
66
- + (float(sample) / (num_samples - 1)) * (end_rgb[2] - start_rgb[2])
66
+ start_rgb[1] + (float(sample) / (num_samples - 1)) * (end_rgb[1] - start_rgb[1])
67
67
  )
68
+ blue = int(start_rgb[2] + (float(sample) / (num_samples - 1)) * (end_rgb[2] - start_rgb[2]))
68
69
  gradient_colors.append(f"#{red:02X}{green:02X}{blue:02X}")
69
70
 
70
71
  return gradient_colors
71
72
 
72
73
 
73
74
  def show_banner(): # pragma: no cover
74
- program_name = os.path.basename(sys.argv[0])
75
+ """Show the banner."""
76
+ program_name = Path(sys.argv[0]).name
75
77
  program_name = "".join((program_name[0:3].upper(), program_name[3:]))
76
78
  figlet = Figlet("georgia11")
77
79
 
78
80
  banner_text = figlet.renderText(program_name)
79
81
 
80
82
  banner_lines = [Text(line) for line in banner_text.splitlines()]
81
- max_line_length = max([len(line) for line in banner_lines])
83
+ max_line_length = max(len(line) for line in banner_lines)
82
84
  half_length = max_line_length // 2
83
85
 
84
86
  colors = gradient("#00C9CD", "#472AFF", half_length) + gradient(
@@ -95,28 +97,30 @@ def show_banner(): # pragma: no cover
95
97
 
96
98
 
97
99
  def get_extension_variables(json_ext_variables):
100
+ """Get the extension variables from the environment."""
98
101
  variables = {}
99
- for var in filter(lambda x: x[0].startswith("EXT_"), os.environ.items()):
102
+ for var in filter(lambda ext_item: ext_item[0].startswith("EXT_"), os.environ.items()):
100
103
  if var[0] in json_ext_variables:
101
104
  try:
102
- value = json.loads(var[1])
105
+ item_value = json.loads(var[1])
103
106
  except json.JSONDecodeError:
104
- raise Exception(f"Variable {var[0]} not well formatted")
107
+ raise VariableNotWellFormedError(f"Variable {var[0]} not well formatted")
105
108
  else:
106
- value = var[1]
109
+ item_value = var[1]
107
110
 
108
- variables[var[0][4:]] = value
111
+ variables[var[0][4:]] = item_value
109
112
  return variables
110
113
 
111
114
 
112
115
  def get_api_url(extension):
116
+ """Get the API URL for the extension."""
113
117
  if extension:
114
- api_url = extension.api.urls
115
- return api_url
118
+ return extension.api.urls
116
119
  return None
117
120
 
118
121
 
119
122
  def get_urlpatterns(extension):
123
+ """Get the URL patterns for the extension."""
120
124
  urlpatterns = [
121
125
  path("admin/", admin.site.urls),
122
126
  ]
@@ -130,19 +134,14 @@ def get_urlpatterns(extension):
130
134
 
131
135
 
132
136
  def get_initializer_function():
133
- """
134
- Dynamically import and return the initializer function from settings.INITIALIZER.
135
- """
137
+ """Dynamically import and return the initializer function from settings.INITIALIZER."""
136
138
  # Read from environment variable instead of Django settings to avoid circular dependency
137
139
  # (Django settings need to be configured before we can read settings.INITIALIZER)
138
- return os.getenv(
139
- "MPT_INITIALIZER", "mpt_extension_sdk.runtime.initializer.initialize"
140
- )
140
+ return os.getenv("MPT_INITIALIZER", "mpt_extension_sdk.runtime.initializer.initialize")
141
141
 
142
142
 
143
- def initialize_extension(
144
- options, group=DEFAULT_APP_CONFIG_GROUP, name=DEFAULT_APP_CONFIG_NAME
145
- ):
143
+ def initialize_extension(options, group=DEFAULT_APP_CONFIG_GROUP, name=DEFAULT_APP_CONFIG_NAME):
144
+ """Initialize the extension."""
146
145
  initialize_path = get_initializer_function()
147
146
  initialize_func = import_string(initialize_path)
148
147
  initialize_func(options, group=group, name=name)