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
|
@@ -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(
|
|
26
|
+
logger.debug("Future for %s has been completed successfully", key)
|
|
26
27
|
return
|
|
27
|
-
logger.error(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
61
|
-
|
|
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
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
79
|
+
"""Get processing orders."""
|
|
77
80
|
orders = []
|
|
78
|
-
rql_query =
|
|
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 ==
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
79
|
-
self.
|
|
80
|
-
|
|
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,
|
|
86
|
-
if not
|
|
87
|
-
if
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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=
|
|
47
|
+
help=help_value,
|
|
47
48
|
add_help_option=False,
|
|
48
|
-
context_settings=
|
|
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,
|
|
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",
|
|
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
|
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
105
|
+
item_value = json.loads(var[1])
|
|
103
106
|
except json.JSONDecodeError:
|
|
104
|
-
raise
|
|
107
|
+
raise VariableNotWellFormedError(f"Variable {var[0]} not well formatted")
|
|
105
108
|
else:
|
|
106
|
-
|
|
109
|
+
item_value = var[1]
|
|
107
110
|
|
|
108
|
-
variables[var[0][4:]] =
|
|
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
|
-
|
|
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
|
-
|
|
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)
|