mpt-extension-sdk 4.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mpt_extension_sdk/__init__.py +0 -0
- mpt_extension_sdk/airtable/__init__.py +0 -0
- mpt_extension_sdk/airtable/wrap_http_error.py +45 -0
- mpt_extension_sdk/constants.py +11 -0
- mpt_extension_sdk/core/__init__.py +0 -0
- mpt_extension_sdk/core/events/__init__.py +0 -0
- mpt_extension_sdk/core/events/dataclasses.py +16 -0
- mpt_extension_sdk/core/events/registry.py +56 -0
- mpt_extension_sdk/core/extension.py +12 -0
- mpt_extension_sdk/core/security.py +50 -0
- mpt_extension_sdk/core/utils.py +17 -0
- mpt_extension_sdk/flows/__init__.py +0 -0
- mpt_extension_sdk/flows/context.py +39 -0
- mpt_extension_sdk/flows/pipeline.py +51 -0
- mpt_extension_sdk/key_vault/__init__.py +0 -0
- mpt_extension_sdk/key_vault/base.py +110 -0
- mpt_extension_sdk/mpt_http/__init__.py +0 -0
- mpt_extension_sdk/mpt_http/base.py +43 -0
- mpt_extension_sdk/mpt_http/mpt.py +530 -0
- mpt_extension_sdk/mpt_http/utils.py +2 -0
- mpt_extension_sdk/mpt_http/wrap_http_error.py +68 -0
- mpt_extension_sdk/runtime/__init__.py +10 -0
- mpt_extension_sdk/runtime/commands/__init__.py +0 -0
- mpt_extension_sdk/runtime/commands/django.py +42 -0
- mpt_extension_sdk/runtime/commands/run.py +44 -0
- mpt_extension_sdk/runtime/djapp/__init__.py +0 -0
- mpt_extension_sdk/runtime/djapp/apps.py +46 -0
- mpt_extension_sdk/runtime/djapp/conf/__init__.py +12 -0
- mpt_extension_sdk/runtime/djapp/conf/default.py +225 -0
- mpt_extension_sdk/runtime/djapp/conf/urls.py +9 -0
- mpt_extension_sdk/runtime/djapp/management/__init__.py +0 -0
- mpt_extension_sdk/runtime/djapp/management/commands/__init__.py +0 -0
- mpt_extension_sdk/runtime/djapp/management/commands/consume_events.py +38 -0
- mpt_extension_sdk/runtime/djapp/middleware.py +21 -0
- mpt_extension_sdk/runtime/events/__init__.py +0 -0
- mpt_extension_sdk/runtime/events/dispatcher.py +83 -0
- mpt_extension_sdk/runtime/events/producers.py +108 -0
- mpt_extension_sdk/runtime/events/utils.py +85 -0
- mpt_extension_sdk/runtime/initializer.py +62 -0
- mpt_extension_sdk/runtime/logging.py +36 -0
- mpt_extension_sdk/runtime/master.py +136 -0
- mpt_extension_sdk/runtime/swoext.py +69 -0
- mpt_extension_sdk/runtime/tracer.py +18 -0
- mpt_extension_sdk/runtime/utils.py +148 -0
- mpt_extension_sdk/runtime/workers.py +90 -0
- mpt_extension_sdk/swo_rql/__init__.py +5 -0
- mpt_extension_sdk/swo_rql/constants.py +7 -0
- mpt_extension_sdk/swo_rql/query_builder.py +392 -0
- mpt_extension_sdk-4.5.0.dist-info/METADATA +45 -0
- mpt_extension_sdk-4.5.0.dist-info/RECORD +53 -0
- mpt_extension_sdk-4.5.0.dist-info/WHEEL +4 -0
- mpt_extension_sdk-4.5.0.dist-info/entry_points.txt +6 -0
- mpt_extension_sdk-4.5.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import rich
|
|
4
|
+
from rich.theme import Theme
|
|
5
|
+
|
|
6
|
+
from mpt_extension_sdk.constants import (
|
|
7
|
+
DEFAULT_APP_CONFIG_GROUP,
|
|
8
|
+
DEFAULT_APP_CONFIG_NAME,
|
|
9
|
+
DJANGO_SETTINGS_MODULE,
|
|
10
|
+
)
|
|
11
|
+
from mpt_extension_sdk.runtime.djapp.conf import extract_product_ids
|
|
12
|
+
from mpt_extension_sdk.runtime.events.utils import instrument_logging
|
|
13
|
+
from mpt_extension_sdk.runtime.utils import (
|
|
14
|
+
get_extension_app_config_name,
|
|
15
|
+
get_extension_variables,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# TODO: Move this out of sdk
|
|
19
|
+
JSON_EXT_VARIABLES = {
|
|
20
|
+
"EXT_WEBHOOKS_SECRETS",
|
|
21
|
+
"EXT_AIRTABLE_BASES",
|
|
22
|
+
"EXT_AIRTABLE_PRICING_BASES",
|
|
23
|
+
"EXT_PRODUCT_SEGMENT",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def initialize(options, group=DEFAULT_APP_CONFIG_GROUP, name=DEFAULT_APP_CONFIG_NAME):
|
|
28
|
+
rich.reconfigure(theme=Theme({"repr.mpt_id": "bold light_salmon3"}))
|
|
29
|
+
django_settings_module = options.get(
|
|
30
|
+
"django_settings_module", DJANGO_SETTINGS_MODULE
|
|
31
|
+
)
|
|
32
|
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", django_settings_module)
|
|
33
|
+
import django
|
|
34
|
+
from django.conf import settings
|
|
35
|
+
|
|
36
|
+
root_logging_handler = "rich" if options.get("color") else "console"
|
|
37
|
+
if settings.USE_APPLICATIONINSIGHTS:
|
|
38
|
+
logging_handlers = [root_logging_handler, "opentelemetry"]
|
|
39
|
+
else:
|
|
40
|
+
logging_handlers = [root_logging_handler]
|
|
41
|
+
|
|
42
|
+
logging_level = "DEBUG" if options.get("debug") else "INFO"
|
|
43
|
+
|
|
44
|
+
app_config_name = get_extension_app_config_name(group=group, name=name)
|
|
45
|
+
app_root_module, _ = app_config_name.split(".", 1)
|
|
46
|
+
settings.DEBUG = options.get("debug", False)
|
|
47
|
+
settings.INSTALLED_APPS.append(app_config_name)
|
|
48
|
+
settings.LOGGING["root"]["handlers"] = logging_handlers
|
|
49
|
+
settings.LOGGING["loggers"]["swo.mpt"]["handlers"] = logging_handlers
|
|
50
|
+
settings.LOGGING["loggers"]["swo.mpt"]["level"] = logging_level
|
|
51
|
+
settings.LOGGING["loggers"][app_root_module] = {
|
|
52
|
+
"handlers": logging_handlers,
|
|
53
|
+
"level": logging_level,
|
|
54
|
+
"propagate": False,
|
|
55
|
+
}
|
|
56
|
+
settings.EXTENSION_CONFIG.update(get_extension_variables(JSON_EXT_VARIABLES))
|
|
57
|
+
settings.MPT_PRODUCTS_IDS = extract_product_ids(settings.MPT_PRODUCTS_IDS)
|
|
58
|
+
|
|
59
|
+
if settings.USE_APPLICATIONINSIGHTS:
|
|
60
|
+
instrument_logging()
|
|
61
|
+
|
|
62
|
+
django.setup()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from rich.highlighter import ReprHighlighter as _ReprHighlighter
|
|
2
|
+
from rich.logging import RichHandler as _RichHandler
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ReprHighlighter(_ReprHighlighter):
|
|
6
|
+
accounts_prefixes = ("ACC", "BUY", "LCE", "MOD", "SEL", "USR", "AUSR", "UGR")
|
|
7
|
+
catalog_prefixes = (
|
|
8
|
+
"PRD",
|
|
9
|
+
"ITM",
|
|
10
|
+
"IGR",
|
|
11
|
+
"PGR",
|
|
12
|
+
"MED",
|
|
13
|
+
"DOC",
|
|
14
|
+
"TCS",
|
|
15
|
+
"TPL",
|
|
16
|
+
"WHO",
|
|
17
|
+
"PRC",
|
|
18
|
+
"LST",
|
|
19
|
+
"AUT",
|
|
20
|
+
"UNT",
|
|
21
|
+
)
|
|
22
|
+
commerce_prefixes = ("AGR", "ORD", "SUB", "REQ")
|
|
23
|
+
aux_prefixes = ("FIL", "MSG")
|
|
24
|
+
all_prefixes = (
|
|
25
|
+
*accounts_prefixes,
|
|
26
|
+
*catalog_prefixes,
|
|
27
|
+
*commerce_prefixes,
|
|
28
|
+
*aux_prefixes,
|
|
29
|
+
)
|
|
30
|
+
highlights = _ReprHighlighter.highlights + [
|
|
31
|
+
rf"(?P<mpt_id>(?:{'|'.join(all_prefixes)})(?:-\d{{4}})*)"
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class RichHandler(_RichHandler):
|
|
36
|
+
HIGHLIGHTER_CLASS = ReprHighlighter
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import signal
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from watchfiles import watch
|
|
9
|
+
from watchfiles.filters import PythonFilter
|
|
10
|
+
from watchfiles.run import start_process
|
|
11
|
+
|
|
12
|
+
from mpt_extension_sdk.runtime.workers import start_event_consumer, start_gunicorn
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
HANDLED_SIGNALS = (signal.SIGINT, signal.SIGTERM)
|
|
18
|
+
PROCESS_CHECK_INTERVAL_SECS = int(os.environ.get("PROCESS_CHECK_INTERVAL_SECS", 5))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _display_path(path): # pragma: no cover
|
|
22
|
+
try:
|
|
23
|
+
return f'"{path.relative_to(Path.cwd())}"'
|
|
24
|
+
except ValueError:
|
|
25
|
+
return f'"{path}"'
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Master:
|
|
29
|
+
def __init__(self, options, settings):
|
|
30
|
+
self.workers = {}
|
|
31
|
+
self.options = options
|
|
32
|
+
self.settings = settings
|
|
33
|
+
self.stop_event = threading.Event()
|
|
34
|
+
self.monitor_event = threading.Event()
|
|
35
|
+
self.watch_filter = PythonFilter(ignore_paths=None)
|
|
36
|
+
self.watcher = watch(
|
|
37
|
+
Path.cwd(),
|
|
38
|
+
watch_filter=self.watch_filter,
|
|
39
|
+
stop_event=self.stop_event,
|
|
40
|
+
yield_on_timeout=True,
|
|
41
|
+
)
|
|
42
|
+
self.monitor_thread = None
|
|
43
|
+
self.setup_signals_handler()
|
|
44
|
+
|
|
45
|
+
match self.options["component"]:
|
|
46
|
+
case "all":
|
|
47
|
+
self.proc_targets = {
|
|
48
|
+
"event-consumer": start_event_consumer,
|
|
49
|
+
"gunicorn": start_gunicorn,
|
|
50
|
+
}
|
|
51
|
+
case "api":
|
|
52
|
+
self.proc_targets = {
|
|
53
|
+
"gunicorn": start_gunicorn,
|
|
54
|
+
}
|
|
55
|
+
case "consumer":
|
|
56
|
+
self.proc_targets = {"event-consumer": start_event_consumer}
|
|
57
|
+
case _:
|
|
58
|
+
self.proc_targets = {
|
|
59
|
+
"event-consumer": start_event_consumer,
|
|
60
|
+
"gunicorn": start_gunicorn,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
def setup_signals_handler(self):
|
|
64
|
+
for sig in HANDLED_SIGNALS:
|
|
65
|
+
signal.signal(sig, self.handle_signal)
|
|
66
|
+
|
|
67
|
+
def handle_signal(self, *args, **kwargs):
|
|
68
|
+
self.stop_event.set()
|
|
69
|
+
|
|
70
|
+
def start(self):
|
|
71
|
+
for worker_type, target in self.proc_targets.items():
|
|
72
|
+
self.start_worker_process(worker_type, target)
|
|
73
|
+
self.monitor_thread = threading.Thread(target=self.monitor_processes)
|
|
74
|
+
self.monitor_event.set()
|
|
75
|
+
self.monitor_thread.start()
|
|
76
|
+
|
|
77
|
+
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}")
|
|
81
|
+
|
|
82
|
+
def monitor_processes(self): # pragma: no cover
|
|
83
|
+
while self.monitor_event.is_set():
|
|
84
|
+
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:
|
|
95
|
+
exited_workers.append(worker_type)
|
|
96
|
+
logger.info(f"{worker_type.capitalize()} worker exited")
|
|
97
|
+
if exited_workers == list(self.workers.keys()):
|
|
98
|
+
self.stop_event.set()
|
|
99
|
+
|
|
100
|
+
time.sleep(PROCESS_CHECK_INTERVAL_SECS)
|
|
101
|
+
|
|
102
|
+
def stop(self):
|
|
103
|
+
self.monitor_event.clear()
|
|
104
|
+
self.monitor_thread.join()
|
|
105
|
+
for worker_type, process in self.workers.items():
|
|
106
|
+
process.stop(sigint_timeout=5, sigkill_timeout=1)
|
|
107
|
+
logger.info(
|
|
108
|
+
f"{worker_type.capitalize()} process with pid {process.pid} stopped."
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def restart(self):
|
|
112
|
+
self.stop()
|
|
113
|
+
self.start()
|
|
114
|
+
|
|
115
|
+
def __iter__(self): # pragma: no cover
|
|
116
|
+
return self
|
|
117
|
+
|
|
118
|
+
def __next__(self): # pragma: no cover
|
|
119
|
+
changes = next(self.watcher)
|
|
120
|
+
if changes:
|
|
121
|
+
return list({Path(c[1]) for c in changes})
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
def run(self): # pragma: no cover
|
|
125
|
+
self.start()
|
|
126
|
+
if self.options.get("reload"):
|
|
127
|
+
for files_changed in self:
|
|
128
|
+
if files_changed:
|
|
129
|
+
logger.warning(
|
|
130
|
+
"Detected changes in %s. Reloading...",
|
|
131
|
+
", ".join(map(_display_path, files_changed)),
|
|
132
|
+
)
|
|
133
|
+
self.restart()
|
|
134
|
+
else:
|
|
135
|
+
self.stop_event.wait()
|
|
136
|
+
self.stop()
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from django.utils.module_loading import import_string
|
|
3
|
+
|
|
4
|
+
from mpt_extension_sdk.runtime import get_version
|
|
5
|
+
from mpt_extension_sdk.runtime.utils import show_banner
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def print_version(ctx, param, value):
|
|
9
|
+
if not value or ctx.resilient_parsing:
|
|
10
|
+
return
|
|
11
|
+
click.echo(f"SoftwareOne Extension CLI, version {get_version()}")
|
|
12
|
+
ctx.exit()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.group(context_settings={"help_option_names": ["-h", "--help"]})
|
|
16
|
+
@click.option(
|
|
17
|
+
"--version",
|
|
18
|
+
is_flag=True,
|
|
19
|
+
expose_value=False,
|
|
20
|
+
is_eager=True,
|
|
21
|
+
callback=print_version,
|
|
22
|
+
)
|
|
23
|
+
@click.pass_context
|
|
24
|
+
def cli(ctx):
|
|
25
|
+
"""SoftwareOne Extension CLI"""
|
|
26
|
+
show_banner()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
for cmd in map(
|
|
30
|
+
import_string,
|
|
31
|
+
(
|
|
32
|
+
"mpt_extension_sdk.runtime.commands.run.run",
|
|
33
|
+
"mpt_extension_sdk.runtime.commands.django.django",
|
|
34
|
+
),
|
|
35
|
+
):
|
|
36
|
+
cli.add_command(cmd)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def make_django_command(name, django_command=None, help=None):
|
|
40
|
+
"A wrapper to convert a Django subcommand a Click command"
|
|
41
|
+
if django_command is None:
|
|
42
|
+
django_command = name
|
|
43
|
+
|
|
44
|
+
@click.command(
|
|
45
|
+
name=name,
|
|
46
|
+
help=help,
|
|
47
|
+
add_help_option=False,
|
|
48
|
+
context_settings=dict(ignore_unknown_options=True),
|
|
49
|
+
)
|
|
50
|
+
@click.argument("management_args", nargs=-1, type=click.UNPROCESSED)
|
|
51
|
+
@click.pass_context
|
|
52
|
+
def inner(ctx, management_args):
|
|
53
|
+
from mpt_extension_sdk.runtime.commands.django import django
|
|
54
|
+
|
|
55
|
+
ctx.params["management_args"] = (django_command,) + management_args
|
|
56
|
+
ctx.forward(django)
|
|
57
|
+
|
|
58
|
+
return inner
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
cli.add_command(make_django_command("shell", help="Open Django console"))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main():
|
|
65
|
+
cli(standalone_mode=False)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
main()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
|
|
3
|
+
from opentelemetry import trace
|
|
4
|
+
|
|
5
|
+
tracer = trace.get_tracer(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def dynamic_trace_span(name_fn):
|
|
9
|
+
def decorator(func):
|
|
10
|
+
@wraps(func)
|
|
11
|
+
def wrapper(*args, **kwargs):
|
|
12
|
+
span_name = name_fn(*args, **kwargs)
|
|
13
|
+
with tracer.start_as_current_span(span_name):
|
|
14
|
+
return func(*args, **kwargs)
|
|
15
|
+
|
|
16
|
+
return wrapper
|
|
17
|
+
|
|
18
|
+
return decorator
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from importlib.metadata import entry_points
|
|
5
|
+
|
|
6
|
+
from django.apps import apps
|
|
7
|
+
from django.contrib import admin
|
|
8
|
+
from django.urls import path
|
|
9
|
+
from django.utils.module_loading import import_string
|
|
10
|
+
from pyfiglet import Figlet
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.text import Text
|
|
13
|
+
|
|
14
|
+
from mpt_extension_sdk.constants import (
|
|
15
|
+
DEFAULT_APP_CONFIG_GROUP,
|
|
16
|
+
DEFAULT_APP_CONFIG_NAME,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_extension_app_config_name(
|
|
21
|
+
group=DEFAULT_APP_CONFIG_GROUP, name=DEFAULT_APP_CONFIG_NAME
|
|
22
|
+
):
|
|
23
|
+
eps = entry_points()
|
|
24
|
+
(app_config_ep,) = eps.select(group=group, name=name)
|
|
25
|
+
app_config = app_config_ep.load()
|
|
26
|
+
return f"{app_config.__module__}.{app_config.__name__}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_extension_app_config(
|
|
30
|
+
group=DEFAULT_APP_CONFIG_GROUP, name=DEFAULT_APP_CONFIG_NAME
|
|
31
|
+
):
|
|
32
|
+
app_config_name = get_extension_app_config_name(group=group, name=name)
|
|
33
|
+
return next(
|
|
34
|
+
filter(
|
|
35
|
+
lambda app: app_config_name
|
|
36
|
+
== f"{app.__class__.__module__}.{app.__class__.__name__}",
|
|
37
|
+
apps.app_configs.values(),
|
|
38
|
+
),
|
|
39
|
+
None,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_extension(group=DEFAULT_APP_CONFIG_GROUP, name=DEFAULT_APP_CONFIG_NAME):
|
|
44
|
+
return get_extension_app_config(group=group, name=name).extension
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_events_registry(group=DEFAULT_APP_CONFIG_GROUP, name=DEFAULT_APP_CONFIG_NAME):
|
|
48
|
+
return get_extension(group=group, name=name).events
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
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))
|
|
54
|
+
gradient_colors = [start_hex]
|
|
55
|
+
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
|
+
)
|
|
60
|
+
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])
|
|
67
|
+
)
|
|
68
|
+
gradient_colors.append(f"#{red:02X}{green:02X}{blue:02X}")
|
|
69
|
+
|
|
70
|
+
return gradient_colors
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def show_banner(): # pragma: no cover
|
|
74
|
+
program_name = os.path.basename(sys.argv[0])
|
|
75
|
+
program_name = "".join((program_name[0:3].upper(), program_name[3:]))
|
|
76
|
+
figlet = Figlet("georgia11")
|
|
77
|
+
|
|
78
|
+
banner_text = figlet.renderText(program_name)
|
|
79
|
+
|
|
80
|
+
banner_lines = [Text(line) for line in banner_text.splitlines()]
|
|
81
|
+
max_line_length = max([len(line) for line in banner_lines])
|
|
82
|
+
half_length = max_line_length // 2
|
|
83
|
+
|
|
84
|
+
colors = gradient("#00C9CD", "#472AFF", half_length) + gradient(
|
|
85
|
+
"#472AFF", "#392D9C", half_length + 1
|
|
86
|
+
)
|
|
87
|
+
console = Console()
|
|
88
|
+
|
|
89
|
+
for line in banner_lines:
|
|
90
|
+
colored_line = Text()
|
|
91
|
+
for idx, line_char in enumerate(line):
|
|
92
|
+
line_char.stylize(colors[idx])
|
|
93
|
+
colored_line = Text.assemble(colored_line, line_char)
|
|
94
|
+
console.print(colored_line)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def get_extension_variables(json_ext_variables):
|
|
98
|
+
variables = {}
|
|
99
|
+
for var in filter(lambda x: x[0].startswith("EXT_"), os.environ.items()):
|
|
100
|
+
if var[0] in json_ext_variables:
|
|
101
|
+
try:
|
|
102
|
+
value = json.loads(var[1])
|
|
103
|
+
except json.JSONDecodeError:
|
|
104
|
+
raise Exception(f"Variable {var[0]} not well formatted")
|
|
105
|
+
else:
|
|
106
|
+
value = var[1]
|
|
107
|
+
|
|
108
|
+
variables[var[0][4:]] = value
|
|
109
|
+
return variables
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_api_url(extension):
|
|
113
|
+
if extension:
|
|
114
|
+
api_url = extension.api.urls
|
|
115
|
+
return api_url
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def get_urlpatterns(extension):
|
|
120
|
+
urlpatterns = [
|
|
121
|
+
path("admin/", admin.site.urls),
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
api_url = get_api_url(extension)
|
|
125
|
+
|
|
126
|
+
if api_url:
|
|
127
|
+
urlpatterns.append(path("api/", api_url))
|
|
128
|
+
|
|
129
|
+
return urlpatterns
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def get_initializer_function():
|
|
133
|
+
"""
|
|
134
|
+
Dynamically import and return the initializer function from settings.INITIALIZER.
|
|
135
|
+
"""
|
|
136
|
+
# Read from environment variable instead of Django settings to avoid circular dependency
|
|
137
|
+
# (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
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def initialize_extension(
|
|
144
|
+
options, group=DEFAULT_APP_CONFIG_GROUP, name=DEFAULT_APP_CONFIG_NAME
|
|
145
|
+
):
|
|
146
|
+
initialize_path = get_initializer_function()
|
|
147
|
+
initialize_func = import_string(initialize_path)
|
|
148
|
+
initialize_func(options, group=group, name=name)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from django.core.management import call_command
|
|
2
|
+
from django.core.wsgi import get_wsgi_application
|
|
3
|
+
from gunicorn.app.base import BaseApplication
|
|
4
|
+
|
|
5
|
+
from mpt_extension_sdk.constants import (
|
|
6
|
+
DEFAULT_APP_CONFIG_GROUP,
|
|
7
|
+
DEFAULT_APP_CONFIG_NAME,
|
|
8
|
+
)
|
|
9
|
+
from mpt_extension_sdk.runtime.utils import initialize_extension
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ExtensionWebApplication(BaseApplication):
|
|
13
|
+
def __init__(self, app, options=None):
|
|
14
|
+
self.options = options or {}
|
|
15
|
+
self.application = app
|
|
16
|
+
super().__init__()
|
|
17
|
+
|
|
18
|
+
def load_config(self):
|
|
19
|
+
config = {
|
|
20
|
+
key: value
|
|
21
|
+
for key, value in self.options.items()
|
|
22
|
+
if key in self.cfg.settings and value is not None
|
|
23
|
+
}
|
|
24
|
+
for key, value in config.items():
|
|
25
|
+
self.cfg.set(key.lower(), value)
|
|
26
|
+
|
|
27
|
+
def load(self):
|
|
28
|
+
return self.application
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def start_event_consumer(options):
|
|
32
|
+
initialize_extension(options)
|
|
33
|
+
call_command("consume_events")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def start_gunicorn(
|
|
37
|
+
options,
|
|
38
|
+
group=DEFAULT_APP_CONFIG_GROUP,
|
|
39
|
+
name=DEFAULT_APP_CONFIG_NAME,
|
|
40
|
+
):
|
|
41
|
+
initialize_extension(options, group=group, name=name)
|
|
42
|
+
|
|
43
|
+
logging_config = {
|
|
44
|
+
"version": 1,
|
|
45
|
+
"disable_existing_loggers": False,
|
|
46
|
+
"formatters": {
|
|
47
|
+
"verbose": {
|
|
48
|
+
"format":
|
|
49
|
+
"{asctime} {name} {levelname} (pid: {process}, thread: {thread}) {message}",
|
|
50
|
+
"style": "{",
|
|
51
|
+
},
|
|
52
|
+
"rich": {
|
|
53
|
+
"format": "%(message)s",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
"handlers": {
|
|
57
|
+
"console": {
|
|
58
|
+
"class": "logging.StreamHandler",
|
|
59
|
+
"formatter": "verbose",
|
|
60
|
+
},
|
|
61
|
+
"rich": {
|
|
62
|
+
"class": "rich.logging.RichHandler",
|
|
63
|
+
"formatter": "rich",
|
|
64
|
+
"log_time_format": lambda x: x.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3],
|
|
65
|
+
"rich_tracebacks": True,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
"root": {
|
|
69
|
+
"handlers": ["rich" if options.get("color") else "console"],
|
|
70
|
+
"level": "INFO",
|
|
71
|
+
},
|
|
72
|
+
"loggers": {
|
|
73
|
+
"gunicorn.access": {
|
|
74
|
+
"handlers": ["rich" if options.get("color") else "console"],
|
|
75
|
+
"level": "INFO",
|
|
76
|
+
"propagate": False,
|
|
77
|
+
},
|
|
78
|
+
"gunicorn.error": {
|
|
79
|
+
"handlers": ["rich" if options.get("color") else "console"],
|
|
80
|
+
"level": "INFO",
|
|
81
|
+
"propagate": False,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
guni_options = {
|
|
87
|
+
"bind": options.get("bind", "0.0.0.0:8080"),
|
|
88
|
+
"logconfig_dict": logging_config,
|
|
89
|
+
}
|
|
90
|
+
ExtensionWebApplication(get_wsgi_application(), options=guni_options).run()
|