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.
Files changed (53) hide show
  1. mpt_extension_sdk/__init__.py +0 -0
  2. mpt_extension_sdk/airtable/__init__.py +0 -0
  3. mpt_extension_sdk/airtable/wrap_http_error.py +45 -0
  4. mpt_extension_sdk/constants.py +11 -0
  5. mpt_extension_sdk/core/__init__.py +0 -0
  6. mpt_extension_sdk/core/events/__init__.py +0 -0
  7. mpt_extension_sdk/core/events/dataclasses.py +16 -0
  8. mpt_extension_sdk/core/events/registry.py +56 -0
  9. mpt_extension_sdk/core/extension.py +12 -0
  10. mpt_extension_sdk/core/security.py +50 -0
  11. mpt_extension_sdk/core/utils.py +17 -0
  12. mpt_extension_sdk/flows/__init__.py +0 -0
  13. mpt_extension_sdk/flows/context.py +39 -0
  14. mpt_extension_sdk/flows/pipeline.py +51 -0
  15. mpt_extension_sdk/key_vault/__init__.py +0 -0
  16. mpt_extension_sdk/key_vault/base.py +110 -0
  17. mpt_extension_sdk/mpt_http/__init__.py +0 -0
  18. mpt_extension_sdk/mpt_http/base.py +43 -0
  19. mpt_extension_sdk/mpt_http/mpt.py +530 -0
  20. mpt_extension_sdk/mpt_http/utils.py +2 -0
  21. mpt_extension_sdk/mpt_http/wrap_http_error.py +68 -0
  22. mpt_extension_sdk/runtime/__init__.py +10 -0
  23. mpt_extension_sdk/runtime/commands/__init__.py +0 -0
  24. mpt_extension_sdk/runtime/commands/django.py +42 -0
  25. mpt_extension_sdk/runtime/commands/run.py +44 -0
  26. mpt_extension_sdk/runtime/djapp/__init__.py +0 -0
  27. mpt_extension_sdk/runtime/djapp/apps.py +46 -0
  28. mpt_extension_sdk/runtime/djapp/conf/__init__.py +12 -0
  29. mpt_extension_sdk/runtime/djapp/conf/default.py +225 -0
  30. mpt_extension_sdk/runtime/djapp/conf/urls.py +9 -0
  31. mpt_extension_sdk/runtime/djapp/management/__init__.py +0 -0
  32. mpt_extension_sdk/runtime/djapp/management/commands/__init__.py +0 -0
  33. mpt_extension_sdk/runtime/djapp/management/commands/consume_events.py +38 -0
  34. mpt_extension_sdk/runtime/djapp/middleware.py +21 -0
  35. mpt_extension_sdk/runtime/events/__init__.py +0 -0
  36. mpt_extension_sdk/runtime/events/dispatcher.py +83 -0
  37. mpt_extension_sdk/runtime/events/producers.py +108 -0
  38. mpt_extension_sdk/runtime/events/utils.py +85 -0
  39. mpt_extension_sdk/runtime/initializer.py +62 -0
  40. mpt_extension_sdk/runtime/logging.py +36 -0
  41. mpt_extension_sdk/runtime/master.py +136 -0
  42. mpt_extension_sdk/runtime/swoext.py +69 -0
  43. mpt_extension_sdk/runtime/tracer.py +18 -0
  44. mpt_extension_sdk/runtime/utils.py +148 -0
  45. mpt_extension_sdk/runtime/workers.py +90 -0
  46. mpt_extension_sdk/swo_rql/__init__.py +5 -0
  47. mpt_extension_sdk/swo_rql/constants.py +7 -0
  48. mpt_extension_sdk/swo_rql/query_builder.py +392 -0
  49. mpt_extension_sdk-4.5.0.dist-info/METADATA +45 -0
  50. mpt_extension_sdk-4.5.0.dist-info/RECORD +53 -0
  51. mpt_extension_sdk-4.5.0.dist-info/WHEEL +4 -0
  52. mpt_extension_sdk-4.5.0.dist-info/entry_points.txt +6 -0
  53. 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()
@@ -0,0 +1,5 @@
1
+ from mpt_extension_sdk.swo_rql.query_builder import RQLQuery
2
+
3
+ R = RQLQuery
4
+
5
+ __all__ = ["R", "RQLQuery"]
@@ -0,0 +1,7 @@
1
+ COMP = ("eq", "ne", "lt", "le", "gt", "ge")
2
+ SEARCH = ("like", "ilike")
3
+ LIST = ("in", "out")
4
+ NULL = "null"
5
+ EMPTY = "empty"
6
+
7
+ KEYWORDS = (*COMP, *SEARCH, *LIST, NULL, EMPTY)