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,46 @@
|
|
|
1
|
+
from django.apps import AppConfig
|
|
2
|
+
from django.conf import settings
|
|
3
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
4
|
+
|
|
5
|
+
from mpt_extension_sdk.core.extension import Extension
|
|
6
|
+
|
|
7
|
+
ext = Extension()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DjAppConfig(AppConfig):
|
|
11
|
+
name = "mpt_extension_sdk.runtime.djapp"
|
|
12
|
+
|
|
13
|
+
def ready(self):
|
|
14
|
+
if not hasattr(settings, "MPT_PRODUCTS_IDS") or not settings.MPT_PRODUCTS_IDS:
|
|
15
|
+
raise ImproperlyConfigured(
|
|
16
|
+
f"Extension {self.verbose_name} is not properly configured."
|
|
17
|
+
"MPT_PRODUCTS_IDS is missing or empty."
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
self.extension_ready()
|
|
21
|
+
|
|
22
|
+
def extension_ready(self):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ExtensionConfig(DjAppConfig):
|
|
27
|
+
name = "mpt_extension_sdk"
|
|
28
|
+
verbose_name = "SWO Extension SDK"
|
|
29
|
+
extension = ext
|
|
30
|
+
|
|
31
|
+
def extension_ready(self):
|
|
32
|
+
error_msgs = []
|
|
33
|
+
|
|
34
|
+
for product_id in settings.MPT_PRODUCTS_IDS:
|
|
35
|
+
if (
|
|
36
|
+
"WEBHOOKS_SECRETS" not in settings.EXTENSION_CONFIG
|
|
37
|
+
or product_id not in settings.EXTENSION_CONFIG["WEBHOOKS_SECRETS"]
|
|
38
|
+
):
|
|
39
|
+
msg = (
|
|
40
|
+
f"The webhook secret for {product_id} is not found. "
|
|
41
|
+
f"Please, specify it in EXT_WEBHOOKS_SECRETS environment variable."
|
|
42
|
+
)
|
|
43
|
+
error_msgs.append(msg)
|
|
44
|
+
|
|
45
|
+
if error_msgs:
|
|
46
|
+
raise ImproperlyConfigured("\n".join(error_msgs))
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def extract_product_ids(product_ids: str) -> list[str]:
|
|
5
|
+
return product_ids.split(",")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_for_product(settings, variable_name: str, product_id: str) -> Any:
|
|
9
|
+
"""
|
|
10
|
+
A shortcut to return product scoped variable from the extension settings.
|
|
11
|
+
"""
|
|
12
|
+
return settings.EXTENSION_CONFIG[variable_name][product_id]
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django settings for pippo project.
|
|
3
|
+
|
|
4
|
+
Generated by 'django-admin startproject' using Django 4.2.8.
|
|
5
|
+
|
|
6
|
+
For more information on this file, see
|
|
7
|
+
https://docs.djangoproject.com/en/4.2/topics/settings/
|
|
8
|
+
|
|
9
|
+
For the full list of settings and their values, see
|
|
10
|
+
https://docs.djangoproject.com/en/4.2/ref/settings/
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from azure.monitor.opentelemetry.exporter import AzureMonitorLogExporter
|
|
17
|
+
from opentelemetry._logs import set_logger_provider
|
|
18
|
+
from opentelemetry.sdk._logs import LoggerProvider
|
|
19
|
+
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
|
|
20
|
+
|
|
21
|
+
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
|
22
|
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Quick-start development settings - unsuitable for production
|
|
26
|
+
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
|
27
|
+
|
|
28
|
+
# SECURITY WARNING: keep the secret key used in production secret!
|
|
29
|
+
SECRET_KEY = os.getenv(
|
|
30
|
+
"MPT_DJANGO_SECRET_KEY",
|
|
31
|
+
"",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# SECURITY WARNING: don't run with debug turned on in production!
|
|
35
|
+
DEBUG = True
|
|
36
|
+
|
|
37
|
+
ALLOWED_HOSTS = ["*"]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Application definition
|
|
41
|
+
|
|
42
|
+
INSTALLED_APPS = [
|
|
43
|
+
"django.contrib.admin",
|
|
44
|
+
"django.contrib.auth",
|
|
45
|
+
"django.contrib.contenttypes",
|
|
46
|
+
"django.contrib.sessions",
|
|
47
|
+
"django.contrib.messages",
|
|
48
|
+
"django.contrib.staticfiles",
|
|
49
|
+
"mpt_extension_sdk.runtime.djapp.apps.DjAppConfig",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
MIDDLEWARE = [
|
|
53
|
+
"django.middleware.security.SecurityMiddleware",
|
|
54
|
+
"django.contrib.sessions.middleware.SessionMiddleware",
|
|
55
|
+
"django.middleware.common.CommonMiddleware",
|
|
56
|
+
"django.middleware.csrf.CsrfViewMiddleware",
|
|
57
|
+
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
|
58
|
+
"django.contrib.messages.middleware.MessageMiddleware",
|
|
59
|
+
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
|
60
|
+
"mpt_extension_sdk.runtime.djapp.middleware.MPTClientMiddleware",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
ROOT_URLCONF = "mpt_extension_sdk.runtime.djapp.conf.urls"
|
|
64
|
+
|
|
65
|
+
TEMPLATES = [
|
|
66
|
+
{
|
|
67
|
+
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
|
68
|
+
"DIRS": [],
|
|
69
|
+
"APP_DIRS": True,
|
|
70
|
+
"OPTIONS": {
|
|
71
|
+
"context_processors": [
|
|
72
|
+
"django.template.context_processors.debug",
|
|
73
|
+
"django.template.context_processors.request",
|
|
74
|
+
"django.contrib.auth.context_processors.auth",
|
|
75
|
+
"django.contrib.messages.context_processors.messages",
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# Database
|
|
83
|
+
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
|
84
|
+
|
|
85
|
+
DATABASES = {}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# Password validation
|
|
89
|
+
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
|
|
90
|
+
|
|
91
|
+
AUTH_PASSWORD_VALIDATORS = [
|
|
92
|
+
{
|
|
93
|
+
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
|
103
|
+
},
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# Internationalization
|
|
108
|
+
# https://docs.djangoproject.com/en/4.2/topics/i18n/
|
|
109
|
+
|
|
110
|
+
LANGUAGE_CODE = "en-us"
|
|
111
|
+
|
|
112
|
+
TIME_ZONE = "UTC"
|
|
113
|
+
|
|
114
|
+
USE_I18N = True
|
|
115
|
+
|
|
116
|
+
USE_TZ = True
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# Static files (CSS, JavaScript, Images)
|
|
120
|
+
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
|
121
|
+
|
|
122
|
+
STATIC_URL = "static/"
|
|
123
|
+
|
|
124
|
+
# Default primary key field type
|
|
125
|
+
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
|
126
|
+
|
|
127
|
+
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
|
128
|
+
|
|
129
|
+
# OpenTelemetry configuration
|
|
130
|
+
APPLICATIONINSIGHTS_CONNECTION_STRING = os.getenv(
|
|
131
|
+
"APPLICATIONINSIGHTS_CONNECTION_STRING", ""
|
|
132
|
+
)
|
|
133
|
+
USE_APPLICATIONINSIGHTS = APPLICATIONINSIGHTS_CONNECTION_STRING != ""
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if USE_APPLICATIONINSIGHTS: # pragma: no cover
|
|
137
|
+
logger_provider = LoggerProvider()
|
|
138
|
+
set_logger_provider(logger_provider)
|
|
139
|
+
exporter = AzureMonitorLogExporter(
|
|
140
|
+
connection_string=APPLICATIONINSIGHTS_CONNECTION_STRING
|
|
141
|
+
)
|
|
142
|
+
logger_provider.add_log_record_processor(BatchLogRecordProcessor(exporter))
|
|
143
|
+
|
|
144
|
+
LOGGING = {
|
|
145
|
+
"version": 1,
|
|
146
|
+
"disable_existing_loggers": False,
|
|
147
|
+
"formatters": {
|
|
148
|
+
"verbose": {
|
|
149
|
+
"format": "{asctime} {name} {levelname} (pid: {process}, thread: {thread}) {message}",
|
|
150
|
+
"style": "{",
|
|
151
|
+
},
|
|
152
|
+
"rich": {
|
|
153
|
+
"format": "{message}",
|
|
154
|
+
"style": "{",
|
|
155
|
+
},
|
|
156
|
+
"opentelemetry": {
|
|
157
|
+
"format": "(pid: {process}, thread: {thread}) {message}",
|
|
158
|
+
"style": "{",
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
"handlers": {
|
|
162
|
+
"console": {
|
|
163
|
+
"class": "logging.StreamHandler",
|
|
164
|
+
"formatter": "verbose",
|
|
165
|
+
},
|
|
166
|
+
"rich": {
|
|
167
|
+
"class": "mpt_extension_sdk.runtime.logging.RichHandler",
|
|
168
|
+
"formatter": "rich",
|
|
169
|
+
"log_time_format": lambda x: x.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3],
|
|
170
|
+
"rich_tracebacks": True,
|
|
171
|
+
},
|
|
172
|
+
"opentelemetry": {
|
|
173
|
+
"class": "opentelemetry.sdk._logs.LoggingHandler",
|
|
174
|
+
"formatter": "opentelemetry",
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
"root": {
|
|
178
|
+
"handlers": ["rich"],
|
|
179
|
+
"level": "WARNING",
|
|
180
|
+
},
|
|
181
|
+
"loggers": {
|
|
182
|
+
"django": {
|
|
183
|
+
"handlers": ["rich"],
|
|
184
|
+
"level": "INFO",
|
|
185
|
+
"propagate": False,
|
|
186
|
+
},
|
|
187
|
+
"swo.mpt": {
|
|
188
|
+
"handlers": ["rich"],
|
|
189
|
+
"level": "DEBUG",
|
|
190
|
+
"propagate": False,
|
|
191
|
+
},
|
|
192
|
+
"azure": {
|
|
193
|
+
"handlers": ["rich"],
|
|
194
|
+
"level": "WARNING",
|
|
195
|
+
"propagate": False,
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# Proxy settings
|
|
201
|
+
USE_X_FORWARDED_HOST = True
|
|
202
|
+
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
|
203
|
+
|
|
204
|
+
# MPT settings
|
|
205
|
+
|
|
206
|
+
MPT_API_BASE_URL = os.getenv("MPT_API_BASE_URL", "http://localhost:8000")
|
|
207
|
+
MPT_API_TOKEN = os.getenv("MPT_API_TOKEN", "change-me!")
|
|
208
|
+
MPT_API_TOKEN_OPERATIONS = os.getenv("MPT_API_TOKEN_OPERATIONS", "change-me!")
|
|
209
|
+
MPT_PRODUCTS_IDS = os.getenv("MPT_PRODUCTS_IDS", "PRD-1111-1111")
|
|
210
|
+
MPT_PORTAL_BASE_URL = os.getenv("MPT_PORTAL_BASE_URL", "https://portal.s1.show")
|
|
211
|
+
MPT_KEY_VAULT_NAME = os.getenv("MPT_KEY_VAULT_NAME", "mpt-key-vault")
|
|
212
|
+
|
|
213
|
+
MPT_ORDERS_API_POLLING_INTERVAL_SECS = int(
|
|
214
|
+
os.getenv("MPT_ORDERS_API_POLLING_INTERVAL_SECS", "120")
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
EXTENSION_CONFIG = {
|
|
218
|
+
"DUE_DATE_DAYS": "30",
|
|
219
|
+
"ORDER_CREATION_WINDOW_HOURS": os.getenv("EXT_ORDER_CREATION_WINDOW_HOURS", "24"),
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
MPT_SETUP_CONTEXTS_FUNC = os.getenv(
|
|
223
|
+
"MPT_SETUP_CONTEXTS_FUNC",
|
|
224
|
+
"mpt_extension_sdk.runtime.events.utils.setup_contexts",
|
|
225
|
+
)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from mpt_extension_sdk.constants import (
|
|
2
|
+
DEFAULT_APP_CONFIG_GROUP,
|
|
3
|
+
DEFAULT_APP_CONFIG_NAME,
|
|
4
|
+
)
|
|
5
|
+
from mpt_extension_sdk.runtime.utils import get_extension, get_urlpatterns
|
|
6
|
+
|
|
7
|
+
extension = get_extension(name=DEFAULT_APP_CONFIG_NAME, group=DEFAULT_APP_CONFIG_GROUP)
|
|
8
|
+
|
|
9
|
+
urlpatterns = get_urlpatterns(extension)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import signal # pragma: no cover
|
|
2
|
+
from threading import Event # pragma: no cover
|
|
3
|
+
|
|
4
|
+
from django.core.management.base import BaseCommand # pragma: no cover
|
|
5
|
+
|
|
6
|
+
from mpt_extension_sdk.constants import CONSUME_EVENTS_HELP_TEXT # pragma: no cover
|
|
7
|
+
from mpt_extension_sdk.runtime.events.dispatcher import Dispatcher # pragma: no cover
|
|
8
|
+
from mpt_extension_sdk.runtime.events.producers import (
|
|
9
|
+
OrderEventProducer, # pragma: no cover
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Command(BaseCommand): # pragma: no cover
|
|
14
|
+
help = CONSUME_EVENTS_HELP_TEXT
|
|
15
|
+
producer_classes = [
|
|
16
|
+
OrderEventProducer,
|
|
17
|
+
]
|
|
18
|
+
producers = []
|
|
19
|
+
|
|
20
|
+
def handle(self, *args, **options):
|
|
21
|
+
self.shutdown_event = Event()
|
|
22
|
+
self.dispatcher = Dispatcher()
|
|
23
|
+
self.dispatcher.start()
|
|
24
|
+
|
|
25
|
+
def shutdown(signum, frame):
|
|
26
|
+
self.shutdown_event.set()
|
|
27
|
+
|
|
28
|
+
signal.signal(signal.SIGTERM, shutdown)
|
|
29
|
+
signal.signal(signal.SIGINT, shutdown)
|
|
30
|
+
for producer_cls in self.producer_classes:
|
|
31
|
+
producer = producer_cls(self.dispatcher)
|
|
32
|
+
self.producers.append(producer)
|
|
33
|
+
producer.start()
|
|
34
|
+
|
|
35
|
+
self.shutdown_event.wait()
|
|
36
|
+
for producer in self.producers:
|
|
37
|
+
producer.stop()
|
|
38
|
+
self.dispatcher.stop()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from django.conf import settings
|
|
2
|
+
|
|
3
|
+
from mpt_extension_sdk.mpt_http.base import MPTClient
|
|
4
|
+
|
|
5
|
+
_CLIENT = None
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MPTClientMiddleware: # pragma: no cover
|
|
9
|
+
def __init__(self, get_response):
|
|
10
|
+
self.get_response = get_response
|
|
11
|
+
|
|
12
|
+
def __call__(self, request):
|
|
13
|
+
global _CLIENT
|
|
14
|
+
if not _CLIENT:
|
|
15
|
+
_CLIENT = MPTClient(
|
|
16
|
+
f"{settings.MPT_API_BASE_URL}/v1/",
|
|
17
|
+
settings.MPT_API_TOKEN,
|
|
18
|
+
)
|
|
19
|
+
request.client = _CLIENT
|
|
20
|
+
response = self.get_response(request)
|
|
21
|
+
return response
|
|
File without changes
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import logging
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
from collections import deque
|
|
6
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
7
|
+
|
|
8
|
+
from mpt_extension_sdk.constants import (
|
|
9
|
+
DEFAULT_APP_CONFIG_GROUP,
|
|
10
|
+
DEFAULT_APP_CONFIG_NAME,
|
|
11
|
+
)
|
|
12
|
+
from mpt_extension_sdk.core.events.dataclasses import Event
|
|
13
|
+
from mpt_extension_sdk.core.events.registry import EventsRegistry
|
|
14
|
+
from mpt_extension_sdk.core.utils import setup_client
|
|
15
|
+
from mpt_extension_sdk.runtime.events.utils import wrap_for_trace
|
|
16
|
+
from mpt_extension_sdk.runtime.utils import get_events_registry
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def done_callback(futures, key, future): # pragma: no cover
|
|
22
|
+
del futures[key]
|
|
23
|
+
exc = future.exception()
|
|
24
|
+
if not exc:
|
|
25
|
+
logger.debug(f"Future for {key} has been completed successfully")
|
|
26
|
+
return
|
|
27
|
+
logger.error(f"Future for {key} has failed: {exc}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Dispatcher:
|
|
31
|
+
def __init__(self, group=DEFAULT_APP_CONFIG_GROUP, name=DEFAULT_APP_CONFIG_NAME):
|
|
32
|
+
self.registry: EventsRegistry = get_events_registry(group=group, name=name)
|
|
33
|
+
self.queue = deque()
|
|
34
|
+
self.futures = {}
|
|
35
|
+
self.executor = ThreadPoolExecutor()
|
|
36
|
+
self.running_event = threading.Event()
|
|
37
|
+
self.processor = threading.Thread(target=self.process_events)
|
|
38
|
+
self.client = setup_client()
|
|
39
|
+
|
|
40
|
+
def start(self):
|
|
41
|
+
self.running_event.set()
|
|
42
|
+
self.processor.start()
|
|
43
|
+
|
|
44
|
+
def stop(self):
|
|
45
|
+
self.running_event.clear()
|
|
46
|
+
self.processor.join()
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def running(self):
|
|
50
|
+
return self.running_event.is_set()
|
|
51
|
+
|
|
52
|
+
def dispatch_event(self, event: Event): # pragma: no cover
|
|
53
|
+
if self.registry.is_event_supported(event.type):
|
|
54
|
+
logger.info(f"event of type {event.type} with id {event.id} accepted")
|
|
55
|
+
self.queue.appendleft((event.type, event))
|
|
56
|
+
|
|
57
|
+
def process_events(self): # pragma: no cover
|
|
58
|
+
while self.running:
|
|
59
|
+
skipped = []
|
|
60
|
+
while len(self.queue) > 0:
|
|
61
|
+
event_type, event = self.queue.pop()
|
|
62
|
+
logger.debug(
|
|
63
|
+
f"got event of type {event_type} ({event.id}) from queue..."
|
|
64
|
+
)
|
|
65
|
+
listener = wrap_for_trace(
|
|
66
|
+
self.registry.get_listener(event_type), event_type
|
|
67
|
+
)
|
|
68
|
+
if (event.type, event.id) not in self.futures:
|
|
69
|
+
future = self.executor.submit(listener, self.client, event)
|
|
70
|
+
self.futures[(event.type, event.id)] = future
|
|
71
|
+
future.add_done_callback(
|
|
72
|
+
functools.partial(
|
|
73
|
+
done_callback, self.futures, (event.type, event.id)
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
else:
|
|
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
|
+
|
|
82
|
+
self.queue.extendleft(skipped)
|
|
83
|
+
time.sleep(0.5)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from django.conf import settings
|
|
9
|
+
|
|
10
|
+
from mpt_extension_sdk.core.events.dataclasses import Event
|
|
11
|
+
from mpt_extension_sdk.core.utils import setup_client
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class EventProducer(ABC):
|
|
17
|
+
def __init__(self, dispatcher):
|
|
18
|
+
self.dispatcher = dispatcher
|
|
19
|
+
self.running_event = threading.Event()
|
|
20
|
+
self.producer = threading.Thread(target=self.produce_events)
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def running(self):
|
|
24
|
+
return self.running_event.is_set()
|
|
25
|
+
|
|
26
|
+
def start(self):
|
|
27
|
+
self.running_event.set()
|
|
28
|
+
self.producer.start()
|
|
29
|
+
|
|
30
|
+
def stop(self):
|
|
31
|
+
self.running_event.clear()
|
|
32
|
+
self.producer.join()
|
|
33
|
+
|
|
34
|
+
@contextmanager
|
|
35
|
+
def sleep(self, secs, interval=0.5): # pragma: no cover
|
|
36
|
+
yield
|
|
37
|
+
sleeped = 0
|
|
38
|
+
while sleeped < secs and self.running_event.is_set():
|
|
39
|
+
time.sleep(interval)
|
|
40
|
+
sleeped += interval
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def produce_events(self):
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def produce_events_with_context(self):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class OrderEventProducer(EventProducer):
|
|
52
|
+
def __init__(self, dispatcher):
|
|
53
|
+
super().__init__(dispatcher)
|
|
54
|
+
self.client = setup_client()
|
|
55
|
+
|
|
56
|
+
def produce_events(self):
|
|
57
|
+
while self.running:
|
|
58
|
+
with self.sleep(settings.MPT_ORDERS_API_POLLING_INTERVAL_SECS):
|
|
59
|
+
orders = self.get_processing_orders()
|
|
60
|
+
logger.info(f"{len(orders)} orders found for processing...")
|
|
61
|
+
for order in orders:
|
|
62
|
+
self.dispatcher.dispatch_event(Event(order["id"], "orders", order))
|
|
63
|
+
|
|
64
|
+
def produce_events_with_context(self): # pragma: no cover
|
|
65
|
+
while self.running:
|
|
66
|
+
with self.sleep(settings.MPT_ORDERS_API_POLLING_INTERVAL_SECS):
|
|
67
|
+
orders = self.get_processing_orders()
|
|
68
|
+
orders, contexts = self.filter_and_enrich(self.client, orders)
|
|
69
|
+
logger.info(f"{len(orders)} orders found for processing...")
|
|
70
|
+
for order, context in zip(orders, contexts):
|
|
71
|
+
self.dispatcher.dispatch_event(
|
|
72
|
+
Event(order["id"], "orders", order, context)
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def get_processing_orders(self):
|
|
76
|
+
products = ",".join(settings.MPT_PRODUCTS_IDS)
|
|
77
|
+
orders = []
|
|
78
|
+
rql_query = f"and(in(agreement.product.id,({products})),eq(status,processing))"
|
|
79
|
+
url = (
|
|
80
|
+
f"/commerce/orders?{rql_query}&select=audit,parameters,lines,subscriptions,"
|
|
81
|
+
f"subscriptions.lines,agreement,buyer,seller&order=audit.created.at"
|
|
82
|
+
)
|
|
83
|
+
page = None
|
|
84
|
+
limit = 10
|
|
85
|
+
offset = 0
|
|
86
|
+
while self.has_more_pages(page):
|
|
87
|
+
try:
|
|
88
|
+
response = self.client.get(f"{url}&limit={limit}&offset={offset}")
|
|
89
|
+
except requests.RequestException:
|
|
90
|
+
logger.exception("Cannot retrieve orders")
|
|
91
|
+
return []
|
|
92
|
+
if response.status_code == 200:
|
|
93
|
+
page = response.json()
|
|
94
|
+
orders.extend(page["data"])
|
|
95
|
+
else:
|
|
96
|
+
logger.warning(
|
|
97
|
+
f"Order API error: {response.status_code} {response.content}"
|
|
98
|
+
)
|
|
99
|
+
return []
|
|
100
|
+
offset += limit
|
|
101
|
+
|
|
102
|
+
return orders
|
|
103
|
+
|
|
104
|
+
def has_more_pages(self, orders):
|
|
105
|
+
if not orders:
|
|
106
|
+
return True
|
|
107
|
+
pagination = orders["$meta"]["pagination"]
|
|
108
|
+
return pagination["total"] > pagination["limit"] + pagination["offset"]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from functools import wraps
|
|
3
|
+
|
|
4
|
+
from azure.monitor.opentelemetry.exporter import (
|
|
5
|
+
AzureMonitorTraceExporter,
|
|
6
|
+
)
|
|
7
|
+
from django.conf import settings
|
|
8
|
+
from opentelemetry import trace
|
|
9
|
+
from opentelemetry.instrumentation.django import DjangoInstrumentor
|
|
10
|
+
from opentelemetry.instrumentation.logging import LoggingInstrumentor
|
|
11
|
+
from opentelemetry.instrumentation.requests import RequestsInstrumentor
|
|
12
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
13
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
14
|
+
|
|
15
|
+
from mpt_extension_sdk.flows.context import Context
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _response_hook(span, request, response): # pragma: no cover
|
|
21
|
+
span.set_attribute(
|
|
22
|
+
"request.header.x-correlation-id",
|
|
23
|
+
request.headers.get("x-correlation-id", ""),
|
|
24
|
+
)
|
|
25
|
+
span.set_attribute(
|
|
26
|
+
"request.header.x-request-id",
|
|
27
|
+
request.headers.get("x-request-id", ""),
|
|
28
|
+
)
|
|
29
|
+
if not response.ok:
|
|
30
|
+
span.set_attribute("request.body", request.body or "")
|
|
31
|
+
span.set_attribute("response.body", response.content or "")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def instrument_logging(): # pragma: no cover
|
|
35
|
+
exporter = AzureMonitorTraceExporter(
|
|
36
|
+
connection_string=settings.APPLICATIONINSIGHTS_CONNECTION_STRING
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
trace_provider = TracerProvider()
|
|
40
|
+
trace_provider.add_span_processor(BatchSpanProcessor(exporter))
|
|
41
|
+
trace.set_tracer_provider(trace_provider)
|
|
42
|
+
|
|
43
|
+
DjangoInstrumentor().instrument()
|
|
44
|
+
RequestsInstrumentor().instrument(response_hook=_response_hook)
|
|
45
|
+
LoggingInstrumentor().instrument(set_logging_format=True)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def wrap_for_trace(func, event_type): # pragma: no cover
|
|
49
|
+
@wraps(func)
|
|
50
|
+
def opentelemetry_wrapper(client, event):
|
|
51
|
+
tracer = trace.get_tracer(event_type)
|
|
52
|
+
object_id = event.id
|
|
53
|
+
|
|
54
|
+
with tracer.start_as_current_span(
|
|
55
|
+
f"Event {event_type} for {object_id}"
|
|
56
|
+
) as span:
|
|
57
|
+
try:
|
|
58
|
+
func(client, event)
|
|
59
|
+
except Exception:
|
|
60
|
+
logger.exception("Unhandled exception!")
|
|
61
|
+
finally:
|
|
62
|
+
if span.is_recording():
|
|
63
|
+
span.set_attribute("order.id", object_id)
|
|
64
|
+
|
|
65
|
+
@wraps(func)
|
|
66
|
+
def wrapper(client, event):
|
|
67
|
+
try:
|
|
68
|
+
func(client, event)
|
|
69
|
+
except Exception:
|
|
70
|
+
logger.exception("Unhandled exception!")
|
|
71
|
+
|
|
72
|
+
return opentelemetry_wrapper if settings.USE_APPLICATIONINSIGHTS else wrapper
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def setup_contexts(mpt_client, orders):
|
|
76
|
+
"""
|
|
77
|
+
List of contexts from orders
|
|
78
|
+
Args:
|
|
79
|
+
mpt_client (MPTClient): MPT client
|
|
80
|
+
orders (list): List of orders
|
|
81
|
+
|
|
82
|
+
Returns: List of contexts
|
|
83
|
+
|
|
84
|
+
"""
|
|
85
|
+
return [Context(order=order) for order in orders]
|