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,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
@@ -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]