mpt-extension-sdk 5.6.1__py3-none-any.whl → 5.8.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 (40) hide show
  1. mpt_extension_sdk/airtable/wrap_http_error.py +7 -4
  2. mpt_extension_sdk/constants.py +2 -0
  3. mpt_extension_sdk/core/events/dataclasses.py +1 -0
  4. mpt_extension_sdk/core/events/registry.py +5 -1
  5. mpt_extension_sdk/core/extension.py +1 -0
  6. mpt_extension_sdk/core/security.py +33 -26
  7. mpt_extension_sdk/core/utils.py +2 -0
  8. mpt_extension_sdk/flows/context.py +11 -1
  9. mpt_extension_sdk/flows/pipeline.py +7 -1
  10. mpt_extension_sdk/key_vault/base.py +35 -25
  11. mpt_extension_sdk/mpt_http/base.py +7 -3
  12. mpt_extension_sdk/mpt_http/mpt.py +79 -24
  13. mpt_extension_sdk/mpt_http/utils.py +1 -0
  14. mpt_extension_sdk/mpt_http/wrap_http_error.py +13 -6
  15. mpt_extension_sdk/runtime/__init__.py +1 -0
  16. mpt_extension_sdk/runtime/commands/django.py +3 -4
  17. mpt_extension_sdk/runtime/commands/run.py +0 -2
  18. mpt_extension_sdk/runtime/djapp/apps.py +5 -1
  19. mpt_extension_sdk/runtime/djapp/conf/__init__.py +2 -3
  20. mpt_extension_sdk/runtime/djapp/conf/default.py +10 -10
  21. mpt_extension_sdk/runtime/djapp/management/commands/consume_events.py +11 -7
  22. mpt_extension_sdk/runtime/djapp/middleware.py +4 -3
  23. mpt_extension_sdk/runtime/errors.py +4 -0
  24. mpt_extension_sdk/runtime/events/dispatcher.py +19 -11
  25. mpt_extension_sdk/runtime/events/producers.py +17 -6
  26. mpt_extension_sdk/runtime/events/utils.py +4 -2
  27. mpt_extension_sdk/runtime/initializer.py +3 -2
  28. mpt_extension_sdk/runtime/logging.py +9 -2
  29. mpt_extension_sdk/runtime/master.py +24 -13
  30. mpt_extension_sdk/runtime/swoext.py +9 -7
  31. mpt_extension_sdk/runtime/tracer.py +1 -0
  32. mpt_extension_sdk/runtime/utils.py +33 -18
  33. mpt_extension_sdk/runtime/workers.py +11 -6
  34. mpt_extension_sdk/swo_rql/query_builder.py +15 -12
  35. {mpt_extension_sdk-5.6.1.dist-info → mpt_extension_sdk-5.8.0.dist-info}/METADATA +1 -1
  36. mpt_extension_sdk-5.8.0.dist-info/RECORD +54 -0
  37. mpt_extension_sdk-5.6.1.dist-info/RECORD +0 -53
  38. {mpt_extension_sdk-5.6.1.dist-info → mpt_extension_sdk-5.8.0.dist-info}/WHEEL +0 -0
  39. {mpt_extension_sdk-5.6.1.dist-info → mpt_extension_sdk-5.8.0.dist-info}/entry_points.txt +0 -0
  40. {mpt_extension_sdk-5.6.1.dist-info → mpt_extension_sdk-5.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -19,15 +19,17 @@ 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."""
31
33
  def __init__(self, group=DEFAULT_APP_CONFIG_GROUP, name=DEFAULT_APP_CONFIG_NAME):
32
34
  self.registry: EventsRegistry = get_events_registry(group=group, name=name)
33
35
  self.queue = deque()
@@ -38,46 +40,52 @@ class Dispatcher:
38
40
  self.client = setup_client()
39
41
 
40
42
  def start(self):
43
+ """Start the dispatcher."""
41
44
  self.running_event.set()
42
45
  self.processor.start()
43
46
 
44
47
  def stop(self):
48
+ """Stop the dispatcher."""
45
49
  self.running_event.clear()
46
50
  self.processor.join()
47
51
 
48
52
  @property
49
53
  def running(self):
54
+ """Return True if the dispatcher is running."""
50
55
  return self.running_event.is_set()
51
56
 
52
57
  def dispatch_event(self, event: Event): # pragma: no cover
58
+ """Dispatch an event to the appropriate listener."""
53
59
  if self.registry.is_event_supported(event.type):
54
- logger.info(f"event of type {event.type} with id {event.id} accepted")
60
+ logger.info("event of type %s with id %s accepted", event.type, event.id)
55
61
  self.queue.appendleft((event.type, event))
56
62
 
57
63
  def process_events(self): # pragma: no cover
64
+ """Process events from the queue."""
58
65
  while self.running:
59
66
  skipped = []
60
67
  while len(self.queue) > 0:
61
68
  event_type, event = self.queue.pop()
62
69
  logger.debug(
63
- f"got event of type {event_type} ({event.id}) from queue..."
70
+ "got event of type %s (%s) from queue...", event_type, event.id
64
71
  )
65
72
  listener = wrap_for_trace(
66
73
  self.registry.get_listener(event_type), event_type
67
74
  )
68
- if (event.type, event.id) not in self.futures:
75
+ if (event.type, event.id) in self.futures:
76
+ logger.info(
77
+ "An event for (%s, %s) is already processing, skip it",
78
+ event.type, event.id
79
+ )
80
+ skipped.append((event.type, event))
81
+ else:
69
82
  future = self.executor.submit(listener, self.client, event)
70
- self.futures[(event.type, event.id)] = future
83
+ self.futures[event.type, event.id] = future
71
84
  future.add_done_callback(
72
85
  functools.partial(
73
86
  done_callback, self.futures, (event.type, event.id)
74
87
  )
75
88
  )
76
- else:
77
- logger.info(
78
- f"An event for {(event.type, event.id)} is already processing, skip it"
79
- )
80
- skipped.append((event.type, event))
81
89
 
82
90
  self.queue.extendleft(skipped)
83
91
  time.sleep(0.5)
@@ -3,6 +3,7 @@ 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
@@ -15,6 +16,7 @@ logger = logging.getLogger(__name__)
15
16
 
16
17
 
17
18
  class EventProducer(ABC):
19
+ """Abstract base class for event producers."""
18
20
  def __init__(self, dispatcher):
19
21
  self.dispatcher = dispatcher
20
22
  self.running_event = threading.Event()
@@ -22,18 +24,22 @@ class EventProducer(ABC):
22
24
 
23
25
  @property
24
26
  def running(self):
27
+ """Return True if the producer is running."""
25
28
  return self.running_event.is_set()
26
29
 
27
30
  def start(self):
31
+ """Start the event producer."""
28
32
  self.running_event.set()
29
33
  self.producer.start()
30
34
 
31
35
  def stop(self):
36
+ """Stop the event producer."""
32
37
  self.running_event.clear()
33
38
  self.producer.join()
34
39
 
35
40
  @contextmanager
36
41
  def sleep(self, secs, interval=0.5): # pragma: no cover
42
+ """Sleep for a given number of seconds."""
37
43
  yield
38
44
  sleeped = 0
39
45
  while sleeped < secs and self.running_event.is_set():
@@ -42,38 +48,42 @@ class EventProducer(ABC):
42
48
 
43
49
  @abstractmethod
44
50
  def produce_events(self):
45
- pass
51
+ """Produce events."""
46
52
 
47
53
  @abstractmethod
48
54
  def produce_events_with_context(self):
49
- pass
55
+ """Produce events with context."""
50
56
 
51
57
 
52
58
  class OrderEventProducer(EventProducer):
59
+ """Order event producer."""
53
60
  def __init__(self, dispatcher):
54
61
  super().__init__(dispatcher)
55
62
  self.client = setup_client()
56
63
 
57
64
  def produce_events(self):
65
+ """Produce order events."""
58
66
  while self.running:
59
67
  with self.sleep(settings.MPT_ORDERS_API_POLLING_INTERVAL_SECS):
60
68
  orders = self.get_processing_orders()
61
- logger.info(f"{len(orders)} orders found for processing...")
69
+ logger.info("%d orders found for processing...", len(orders))
62
70
  for order in orders:
63
71
  self.dispatcher.dispatch_event(Event(order["id"], "orders", order))
64
72
 
65
73
  def produce_events_with_context(self): # pragma: no cover
74
+ """Produce order events with context."""
66
75
  while self.running:
67
76
  with self.sleep(settings.MPT_ORDERS_API_POLLING_INTERVAL_SECS):
68
77
  orders = self.get_processing_orders()
69
78
  orders, contexts = self.filter_and_enrich(self.client, orders)
70
- logger.info(f"{len(orders)} orders found for processing...")
79
+ logger.info("%d orders found for processing...", len(orders))
71
80
  for order, context in zip(orders, contexts, strict=False):
72
81
  self.dispatcher.dispatch_event(
73
82
  Event(order["id"], "orders", order, context)
74
83
  )
75
84
 
76
85
  def get_processing_orders(self):
86
+ """Get processing orders."""
77
87
  orders = []
78
88
  rql_query = RQLQuery().agreement.product.id.in_(
79
89
  settings.MPT_PRODUCTS_IDS
@@ -91,12 +101,12 @@ class OrderEventProducer(EventProducer):
91
101
  except requests.RequestException:
92
102
  logger.exception("Cannot retrieve orders")
93
103
  return []
94
- if response.status_code == 200:
104
+ if response.status_code == HTTPStatus.OK.value:
95
105
  page = response.json()
96
106
  orders.extend(page["data"])
97
107
  else:
98
108
  logger.warning(
99
- f"Order API error: {response.status_code} {response.content}"
109
+ "Order API error: %s %s", response.status_code, response.content
100
110
  )
101
111
  return []
102
112
  offset += limit
@@ -104,6 +114,7 @@ class OrderEventProducer(EventProducer):
104
114
  return orders
105
115
 
106
116
  def has_more_pages(self, orders):
117
+ """Check if there are more pages of orders."""
107
118
  if not orders:
108
119
  return True
109
120
  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,6 +47,7 @@ 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."""
49
51
  @wraps(func)
50
52
  def opentelemetry_wrapper(client, event):
51
53
  tracer = trace.get_tracer(event_type)
@@ -74,12 +76,12 @@ def wrap_for_trace(func, event_type): # pragma: no cover
74
76
 
75
77
  def setup_contexts(mpt_client, orders):
76
78
  """
77
- List of contexts from orders
79
+ List of contexts from orders.
80
+
78
81
  Args:
79
82
  mpt_client (MPTClient): MPT client
80
83
  orders (list): List of orders
81
84
 
82
85
  Returns: List of contexts
83
-
84
86
  """
85
87
  return [Context(order=order) for order in orders]
@@ -25,13 +25,14 @@ 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
30
  django_settings_module = options.get(
30
31
  "django_settings_module", DJANGO_SETTINGS_MODULE
31
32
  )
32
33
  os.environ.setdefault("DJANGO_SETTINGS_MODULE", django_settings_module)
33
- import django
34
- from django.conf import settings
34
+ import django # noqa: PLC0415
35
+ from django.conf import settings # noqa: PLC0415
35
36
 
36
37
  root_logging_handler = "rich" if options.get("color") else "console"
37
38
  if settings.USE_APPLICATIONINSIGHTS:
@@ -1,8 +1,11 @@
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."""
6
9
  accounts_prefixes = ("ACC", "BUY", "LCE", "MOD", "SEL", "USR", "AUSR", "UGR")
7
10
  catalog_prefixes = (
8
11
  "PRD",
@@ -27,10 +30,14 @@ class ReprHighlighter(_ReprHighlighter):
27
30
  *commerce_prefixes,
28
31
  *aux_prefixes,
29
32
  )
30
- highlights = _ReprHighlighter.highlights + [
31
- rf"(?P<mpt_id>(?:{'|'.join(all_prefixes)})(?:-\d{{4}})*)"
33
+ prefixes_pattern = "|".join(all_prefixes)
34
+ pattern = rf"(?P<mpt_id>(?:{prefixes_pattern})(?:-\d{{4}})*)"
35
+ highlights: ClassVar[list[str]] = [
36
+ *_ReprHighlighter.highlights,
37
+ pattern,
32
38
  ]
33
39
 
34
40
 
35
41
  class RichHandler(_RichHandler):
42
+ """Rich handler for logging with color support."""
36
43
  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,7 @@ def _display_path(path): # pragma: no cover
26
26
 
27
27
 
28
28
  class Master:
29
+ """Master process for managing worker processes."""
29
30
  def __init__(self, options, settings):
30
31
  self.workers = {}
31
32
  self.options = options
@@ -61,13 +62,16 @@ class Master:
61
62
  }
62
63
 
63
64
  def setup_signals_handler(self):
65
+ """Setup signal handlers for termination signals."""
64
66
  for sig in HANDLED_SIGNALS:
65
67
  signal.signal(sig, self.handle_signal)
66
68
 
67
69
  def handle_signal(self, *args, **kwargs):
70
+ """Handle termination signals."""
68
71
  self.stop_event.set()
69
72
 
70
73
  def start(self):
74
+ """Start all worker processes."""
71
75
  for worker_type, target in self.proc_targets.items():
72
76
  self.start_worker_process(worker_type, target)
73
77
  self.monitor_thread = threading.Thread(target=self.monitor_processes)
@@ -75,40 +79,46 @@ class Master:
75
79
  self.monitor_thread.start()
76
80
 
77
81
  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}")
82
+ """Start a worker process."""
83
+ worker_proc = start_process(target, "function", (self.options,), {})
84
+ self.workers[worker_type] = worker_proc
85
+ logger.info("%s worker pid: %s", worker_type.capitalize(), worker_proc.pid)
81
86
 
82
87
  def monitor_processes(self): # pragma: no cover
88
+ """Monitor the status of worker processes."""
83
89
  while self.monitor_event.is_set():
84
90
  exited_workers = []
85
- for worker_type, p in self.workers.items():
86
- if not p.is_alive():
87
- if p.exitcode != 0:
91
+ for worker_type, worker_proc in self.workers.items():
92
+ if not worker_proc.is_alive():
93
+ if worker_proc.exitcode == 0:
94
+ exited_workers.append(worker_type)
95
+ logger.info("%s worker exited", worker_type.capitalize())
96
+ else:
88
97
  logger.info(
89
- f"Process of type {worker_type} is dead, restart it"
98
+ "Process of type %s is dead, restart it", worker_type
90
99
  )
91
100
  self.start_worker_process(
92
101
  worker_type, self.proc_targets[worker_type]
93
102
  )
94
- else:
95
- exited_workers.append(worker_type)
96
- logger.info(f"{worker_type.capitalize()} worker exited")
97
103
  if exited_workers == list(self.workers.keys()):
98
104
  self.stop_event.set()
99
105
 
100
106
  time.sleep(PROCESS_CHECK_INTERVAL_SECS)
101
107
 
102
108
  def stop(self):
109
+ """Stop all worker processes."""
103
110
  self.monitor_event.clear()
104
111
  self.monitor_thread.join()
105
112
  for worker_type, process in self.workers.items():
106
113
  process.stop(sigint_timeout=5, sigkill_timeout=1)
107
114
  logger.info(
108
- f"{worker_type.capitalize()} process with pid {process.pid} stopped."
115
+ "%s process with pid %s stopped.",
116
+ worker_type.capitalize(),
117
+ process.pid,
109
118
  )
110
119
 
111
120
  def restart(self):
121
+ """Restart the master process."""
112
122
  self.stop()
113
123
  self.start()
114
124
 
@@ -118,10 +128,11 @@ class Master:
118
128
  def __next__(self): # pragma: no cover
119
129
  changes = next(self.watcher)
120
130
  if changes:
121
- return list({Path(c[1]) for c in changes})
131
+ return list({Path(change[1]) for change in changes})
122
132
  return None
123
133
 
124
134
  def run(self): # pragma: no cover
135
+ """Run the master process."""
125
136
  self.start()
126
137
  if self.options.get("reload"):
127
138
  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
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,7 @@ tracer = trace.get_tracer(__name__)
6
6
 
7
7
 
8
8
  def dynamic_trace_span(name_fn):
9
+ """Dynamically create a trace span."""
9
10
  def decorator(func):
10
11
  @wraps(func)
11
12
  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,12 +15,15 @@ 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
23
  def get_extension_app_config_name(
21
24
  group=DEFAULT_APP_CONFIG_GROUP, name=DEFAULT_APP_CONFIG_NAME
22
25
  ):
26
+ """Get the extension app config name for the specified group and name."""
23
27
  eps = entry_points()
24
28
  (app_config_ep,) = eps.select(group=group, name=name)
25
29
  app_config = app_config_ep.load()
@@ -29,28 +33,37 @@ def get_extension_app_config_name(
29
33
  def get_extension_app_config(
30
34
  group=DEFAULT_APP_CONFIG_GROUP, name=DEFAULT_APP_CONFIG_NAME
31
35
  ):
36
+ """Get the extension app config for the specified group and name."""
32
37
  app_config_name = get_extension_app_config_name(group=group, name=name)
33
38
  return next(
34
39
  filter(
35
40
  lambda app: app_config_name
36
- == f"{app.__class__.__module__}.{app.__class__.__name__}",
41
+ == get_app_name(app),
37
42
  apps.app_configs.values(),
38
43
  ),
39
44
  None,
40
45
  )
41
46
 
42
47
 
48
+ def get_app_name(app):
49
+ """Get the app name for the specified app."""
50
+ return f"{app.__class__.__module__}.{app.__class__.__name__}"
51
+
52
+
43
53
  def get_extension(group=DEFAULT_APP_CONFIG_GROUP, name=DEFAULT_APP_CONFIG_NAME):
54
+ """Get the extension for the specified group and name."""
44
55
  return get_extension_app_config(group=group, name=name).extension
45
56
 
46
57
 
47
58
  def get_events_registry(group=DEFAULT_APP_CONFIG_GROUP, name=DEFAULT_APP_CONFIG_NAME):
59
+ """Get the events registry for the extension."""
48
60
  return get_extension(group=group, name=name).events
49
61
 
50
62
 
51
63
  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))
64
+ """Retrieve the gradient."""
65
+ start_rgb = tuple(int(start_hex[idx : idx + 2], GRADIENT_HEX_BASE) for idx in range(1, 6, 2))
66
+ end_rgb = tuple(int(end_hex[idx : idx + 2], GRADIENT_HEX_BASE) for idx in range(1, 6, 2))
54
67
  gradient_colors = [start_hex]
55
68
  for sample in range(1, num_samples):
56
69
  red = int(
@@ -71,14 +84,15 @@ def gradient(start_hex, end_hex, num_samples=10): # pragma: no cover
71
84
 
72
85
 
73
86
  def show_banner(): # pragma: no cover
74
- program_name = os.path.basename(sys.argv[0])
87
+ """Show the banner."""
88
+ program_name = Path(sys.argv[0]).name
75
89
  program_name = "".join((program_name[0:3].upper(), program_name[3:]))
76
90
  figlet = Figlet("georgia11")
77
91
 
78
92
  banner_text = figlet.renderText(program_name)
79
93
 
80
94
  banner_lines = [Text(line) for line in banner_text.splitlines()]
81
- max_line_length = max([len(line) for line in banner_lines])
95
+ max_line_length = max(len(line) for line in banner_lines)
82
96
  half_length = max_line_length // 2
83
97
 
84
98
  colors = gradient("#00C9CD", "#472AFF", half_length) + gradient(
@@ -88,36 +102,38 @@ def show_banner(): # pragma: no cover
88
102
 
89
103
  for line in banner_lines:
90
104
  colored_line = Text()
91
- for i in range(len(line)):
92
- char = line[i : i + 1]
93
- char.stylize(colors[i])
105
+ for idx, line_char in enumerate(line):
106
+ char = Text(line_char)
107
+ char.stylize(colors[idx])
94
108
  colored_line = Text.assemble(colored_line, char)
95
109
  console.print(colored_line)
96
110
 
97
111
 
98
112
  def get_extension_variables(json_ext_variables):
113
+ """Get the extension variables from the environment."""
99
114
  variables = {}
100
- for var in filter(lambda x: x[0].startswith("EXT_"), os.environ.items()):
115
+ for var in filter(lambda ext_item: ext_item[0].startswith("EXT_"), os.environ.items()):
101
116
  if var[0] in json_ext_variables:
102
117
  try:
103
- value = json.loads(var[1])
118
+ item_value = json.loads(var[1])
104
119
  except json.JSONDecodeError:
105
- raise Exception(f"Variable {var[0]} not well formatted")
120
+ raise VariableNotWellFormedError(f"Variable {var[0]} not well formatted")
106
121
  else:
107
- value = var[1]
122
+ item_value = var[1]
108
123
 
109
- variables[var[0][4:]] = value
124
+ variables[var[0][4:]] = item_value
110
125
  return variables
111
126
 
112
127
 
113
128
  def get_api_url(extension):
129
+ """Get the API URL for the extension."""
114
130
  if extension:
115
- api_url = extension.api.urls
116
- return api_url
131
+ return extension.api.urls
117
132
  return None
118
133
 
119
134
 
120
135
  def get_urlpatterns(extension):
136
+ """Get the URL patterns for the extension."""
121
137
  urlpatterns = [
122
138
  path("admin/", admin.site.urls),
123
139
  ]
@@ -131,9 +147,7 @@ def get_urlpatterns(extension):
131
147
 
132
148
 
133
149
  def get_initializer_function():
134
- """
135
- Dynamically import and return the initializer function from settings.INITIALIZER.
136
- """
150
+ """Dynamically import and return the initializer function from settings.INITIALIZER."""
137
151
  # Read from environment variable instead of Django settings to avoid circular dependency
138
152
  # (Django settings need to be configured before we can read settings.INITIALIZER)
139
153
  return os.getenv(
@@ -144,6 +158,7 @@ def get_initializer_function():
144
158
  def initialize_extension(
145
159
  options, group=DEFAULT_APP_CONFIG_GROUP, name=DEFAULT_APP_CONFIG_NAME
146
160
  ):
161
+ """Initialize the extension."""
147
162
  initialize_path = get_initializer_function()
148
163
  initialize_func = import_string(initialize_path)
149
164
  initialize_func(options, group=group, name=name)
@@ -10,25 +10,29 @@ from mpt_extension_sdk.runtime.utils import initialize_extension
10
10
 
11
11
 
12
12
  class ExtensionWebApplication(BaseApplication):
13
+ """Gunicorn application for the extension."""
13
14
  def __init__(self, app, options=None):
14
15
  self.options = options or {}
15
16
  self.application = app
16
17
  super().__init__()
17
18
 
18
19
  def load_config(self):
20
+ """Load configuration settings."""
19
21
  config = {
20
- key: value
21
- for key, value in self.options.items()
22
- if key in self.cfg.settings and value is not None
22
+ opt_key: opt_value
23
+ for opt_key, opt_value in self.options.items()
24
+ if opt_key in self.cfg.settings and opt_value is not None
23
25
  }
24
- for key, value in config.items():
25
- self.cfg.set(key.lower(), value)
26
+ for opt_key, opt_value in config.items():
27
+ self.cfg.set(opt_key.lower(), opt_value)
26
28
 
27
29
  def load(self):
30
+ """Load the application."""
28
31
  return self.application
29
32
 
30
33
 
31
34
  def start_event_consumer(options):
35
+ """Start the event consumer."""
32
36
  initialize_extension(options)
33
37
  call_command("consume_events")
34
38
 
@@ -38,6 +42,7 @@ def start_gunicorn(
38
42
  group=DEFAULT_APP_CONFIG_GROUP,
39
43
  name=DEFAULT_APP_CONFIG_NAME,
40
44
  ):
45
+ """Start the Gunicorn server."""
41
46
  initialize_extension(options, group=group, name=name)
42
47
 
43
48
  logging_config = {
@@ -61,7 +66,7 @@ def start_gunicorn(
61
66
  "rich": {
62
67
  "class": "rich.logging.RichHandler",
63
68
  "formatter": "rich",
64
- "log_time_format": lambda x: x.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3],
69
+ "log_time_format": lambda log_time: log_time.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3],
65
70
  "rich_tracebacks": True,
66
71
  },
67
72
  },