mpt-extension-sdk 4.3.4__tar.gz → 4.4.0__tar.gz

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 (52) hide show
  1. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/PKG-INFO +1 -1
  2. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/constants.py +5 -1
  3. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/mpt_http/mpt.py +90 -49
  4. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/runtime/commands/django.py +17 -4
  5. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/runtime/commands/run.py +13 -10
  6. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/runtime/djapp/conf/default.py +16 -9
  7. mpt_extension_sdk-4.4.0/mpt_extension_sdk/runtime/djapp/conf/urls.py +9 -0
  8. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/runtime/djapp/management/commands/consume_events.py +8 -6
  9. mpt_extension_sdk-4.4.0/mpt_extension_sdk/runtime/events/__init__.py +0 -0
  10. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/runtime/events/dispatcher.py +7 -3
  11. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/runtime/events/producers.py +1 -1
  12. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/runtime/initializer.py +1 -3
  13. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/runtime/master.py +7 -7
  14. mpt_extension_sdk-4.4.0/mpt_extension_sdk/runtime/tracer.py +18 -0
  15. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/runtime/utils.py +45 -4
  16. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/runtime/workers.py +14 -5
  17. mpt_extension_sdk-4.4.0/mpt_extension_sdk/swo_rql/__init__.py +5 -0
  18. mpt_extension_sdk-4.4.0/mpt_extension_sdk/swo_rql/constants.py +7 -0
  19. mpt_extension_sdk-4.4.0/mpt_extension_sdk/swo_rql/query_builder.py +392 -0
  20. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/pyproject.toml +2 -2
  21. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/LICENSE +0 -0
  22. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/README.md +0 -0
  23. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/__init__.py +0 -0
  24. {mpt_extension_sdk-4.3.4/mpt_extension_sdk/core → mpt_extension_sdk-4.4.0/mpt_extension_sdk/airtable}/__init__.py +0 -0
  25. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/airtable/wrap_http_error.py +0 -0
  26. {mpt_extension_sdk-4.3.4/mpt_extension_sdk/core/events → mpt_extension_sdk-4.4.0/mpt_extension_sdk/core}/__init__.py +0 -0
  27. {mpt_extension_sdk-4.3.4/mpt_extension_sdk/flows → mpt_extension_sdk-4.4.0/mpt_extension_sdk/core/events}/__init__.py +0 -0
  28. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/core/events/dataclasses.py +0 -0
  29. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/core/events/registry.py +0 -0
  30. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/core/extension.py +0 -0
  31. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/core/security.py +0 -0
  32. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/core/utils.py +0 -0
  33. {mpt_extension_sdk-4.3.4/mpt_extension_sdk/key_vault → mpt_extension_sdk-4.4.0/mpt_extension_sdk/flows}/__init__.py +0 -0
  34. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/flows/context.py +0 -0
  35. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/flows/pipeline.py +0 -0
  36. {mpt_extension_sdk-4.3.4/mpt_extension_sdk/mpt_http → mpt_extension_sdk-4.4.0/mpt_extension_sdk/key_vault}/__init__.py +0 -0
  37. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/key_vault/base.py +0 -0
  38. {mpt_extension_sdk-4.3.4/mpt_extension_sdk/runtime/commands → mpt_extension_sdk-4.4.0/mpt_extension_sdk/mpt_http}/__init__.py +0 -0
  39. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/mpt_http/base.py +0 -0
  40. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/mpt_http/utils.py +0 -0
  41. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/mpt_http/wrap_http_error.py +0 -0
  42. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/runtime/__init__.py +0 -0
  43. {mpt_extension_sdk-4.3.4/mpt_extension_sdk/runtime/djapp → mpt_extension_sdk-4.4.0/mpt_extension_sdk/runtime/commands}/__init__.py +0 -0
  44. {mpt_extension_sdk-4.3.4/mpt_extension_sdk/runtime/djapp/management → mpt_extension_sdk-4.4.0/mpt_extension_sdk/runtime/djapp}/__init__.py +0 -0
  45. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/runtime/djapp/apps.py +0 -0
  46. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/runtime/djapp/conf/__init__.py +0 -0
  47. {mpt_extension_sdk-4.3.4/mpt_extension_sdk/runtime/djapp/management/commands → mpt_extension_sdk-4.4.0/mpt_extension_sdk/runtime/djapp/management}/__init__.py +0 -0
  48. {mpt_extension_sdk-4.3.4/mpt_extension_sdk/runtime/events → mpt_extension_sdk-4.4.0/mpt_extension_sdk/runtime/djapp/management/commands}/__init__.py +0 -0
  49. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/runtime/djapp/middleware.py +0 -0
  50. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/runtime/events/utils.py +0 -0
  51. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/runtime/logging.py +0 -0
  52. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.4.0}/mpt_extension_sdk/runtime/swoext.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: mpt-extension-sdk
3
- Version: 4.3.4
3
+ Version: 4.4.0
4
4
  Summary: Extensions SDK for SoftwareONE Marketplace Platform
5
5
  License: Apache-2.0
6
6
  Author: SoftwareOne AG
@@ -2,6 +2,10 @@ EVENT_TYPES = "orders"
2
2
  SECURITY_ALGORITHM = "HS256"
3
3
  USER_AGENT = "swo-extensions/1.0"
4
4
  CONSUME_EVENTS_HELP_TEXT = "Consume events from the MPT platform"
5
- DEFAULT_APP_CONFIG_GROUP = "swo.mpt.sdk"
5
+ DEFAULT_APP_CONFIG_GROUP = "swo.mpt.ext"
6
6
  DEFAULT_APP_CONFIG_NAME = "app_config"
7
7
  DJANGO_SETTINGS_MODULE = "mpt_extension_sdk.runtime.djapp.conf.default"
8
+ ERR_DJANGO_SETTINGS_MODULE_TEXT = (
9
+ "DJANGO_SETTINGS_MODULE environment variable is not set. "
10
+ "Please set it to your Django settings module before running this command."
11
+ )
@@ -1,6 +1,5 @@
1
1
  import logging
2
- from datetime import date
3
- from enum import Enum
2
+ from collections.abc import Iterable
4
3
  from functools import cache
5
4
  from itertools import batched
6
5
 
@@ -11,8 +10,6 @@ from mpt_extension_sdk.mpt_http.wrap_http_error import wrap_mpt_http_error
11
10
 
12
11
  logger = logging.getLogger(__name__)
13
12
 
14
- NotifyCategories = Enum("NotifyCategories", settings.MPT_NOTIFY_CATEGORIES)
15
-
16
13
 
17
14
  def _has_more_pages(page):
18
15
  if not page:
@@ -104,6 +101,22 @@ def set_processing_template(mpt_client, order_id, template):
104
101
  return response.json()
105
102
 
106
103
 
104
+ @wrap_mpt_http_error
105
+ def create_asset(mpt_client, order_id, asset):
106
+ """Create a new asset for an order."""
107
+ response = mpt_client.post(f"/commerce/orders/{order_id}/assets", json=asset)
108
+ response.raise_for_status()
109
+ return response.json()
110
+
111
+
112
+ @wrap_mpt_http_error
113
+ def update_asset(mpt_client, order_id, asset_id, **kwargs):
114
+ """Update an order asset."""
115
+ response = mpt_client.put(f"/commerce/orders/{order_id}/assets/{asset_id}", json=kwargs)
116
+ response.raise_for_status()
117
+ return response.json()
118
+
119
+
107
120
  @wrap_mpt_http_error
108
121
  def create_subscription(mpt_client, order_id, subscription):
109
122
  response = mpt_client.post(
@@ -125,9 +138,8 @@ def update_subscription(mpt_client, order_id, subscription_id, **kwargs):
125
138
 
126
139
 
127
140
  @wrap_mpt_http_error
128
- def get_order_subscription_by_external_id(
129
- mpt_client, order_id, subscription_external_id
130
- ):
141
+ def get_order_subscription_by_external_id(mpt_client, order_id, subscription_external_id):
142
+ """Retrieve an order subscription by its external ID."""
131
143
  response = mpt_client.get(
132
144
  f"/commerce/orders/{order_id}/subscriptions?eq(externalIds.vendor,{subscription_external_id})&limit=1",
133
145
  )
@@ -139,9 +151,9 @@ def get_order_subscription_by_external_id(
139
151
 
140
152
  @wrap_mpt_http_error
141
153
  def get_product_items_by_skus(mpt_client, product_id, skus):
142
- rql_query = (
143
- f"and(eq(product.id,{product_id}),in(externalIds.vendor,({','.join(skus)})))"
144
- )
154
+ """Retrieve product items by their SKUs."""
155
+ skus_str = ",".join(skus)
156
+ rql_query = f"and(eq(product.id,{product_id}),in(externalIds.vendor,({skus_str})))"
145
157
  url = f"/catalog/items?{rql_query}"
146
158
  return _paginated(mpt_client, url)
147
159
 
@@ -168,6 +180,14 @@ def get_product_template_or_default(mpt_client, product_id, status, name=None):
168
180
  return templates["data"][0]
169
181
 
170
182
 
183
+ def get_template_by_name(mpt_client, product_id, template_name):
184
+ url = f"/catalog/products/{product_id}/templates?eq(name,{template_name})"
185
+ response = mpt_client.get(url)
186
+ response.raise_for_status()
187
+ templates = response.json()
188
+ return templates["data"][0]
189
+
190
+
171
191
  @wrap_mpt_http_error
172
192
  def update_agreement(mpt_client, agreement_id, **kwargs):
173
193
  response = mpt_client.put(
@@ -184,21 +204,6 @@ def get_agreements_by_query(mpt_client, query):
184
204
  return _paginated(mpt_client, url)
185
205
 
186
206
 
187
- def get_agreements_by_next_sync(mpt_client, next_sync_parameter):
188
- today = date.today().isoformat()
189
- param_condition = (
190
- f"any(parameters.fulfillment,and(eq(externalId,{next_sync_parameter})"
191
- f",lt(displayValue,{today})))"
192
- )
193
- status_condition = "eq(status,Active)"
194
-
195
- rql_query = (
196
- f"and({status_condition},{param_condition})"
197
- "&select=lines,parameters,subscriptions,product,listing"
198
- )
199
- return get_agreements_by_query(mpt_client, rql_query)
200
-
201
-
202
207
  @wrap_mpt_http_error
203
208
  def update_agreement_subscription(mpt_client, subscription_id, **kwargs):
204
209
  response = mpt_client.put(
@@ -237,6 +242,40 @@ def get_product_onetime_items_by_ids(mpt_client, product_id, item_ids):
237
242
  return _paginated(mpt_client, url)
238
243
 
239
244
 
245
+ @wrap_mpt_http_error
246
+ def get_product_items_by_period(
247
+ mpt_client,
248
+ product_id: str,
249
+ period: str,
250
+ vendor_external_ids: Iterable[str] | None = None,
251
+ ):
252
+ """
253
+ Fetches product items based on a specified period and filters.
254
+
255
+ Args:
256
+ mpt_client: Client in tance to interact with the required API.
257
+ product_id (str): The unique identifier of the product to fetch items for.
258
+ period (str): The period for which to fetch the product items.
259
+ vendor_external_ids (Iterable[str] | None):
260
+ Optional. A list of vendor external IDs to filter out the product items by. Defaults
261
+ to None.
262
+
263
+ Returns:
264
+ list:
265
+ A paginated list of product items matching the specified criteria.
266
+
267
+ """
268
+ product_cond = f"eq(product.id,{product_id})"
269
+ vendors_cond = ""
270
+ if vendor_external_ids:
271
+ vendor_ids = ",".join(vendor_external_ids)
272
+ vendors_cond = f",in(externalIds.vendor,({vendor_ids})))"
273
+ rql_query = f"and({product_cond},eq(terms.period,{period}){vendors_cond})"
274
+ url = f"/catalog/items?{rql_query}"
275
+
276
+ return _paginated(mpt_client, url)
277
+
278
+
240
279
  def get_agreements_by_ids(mpt_client, ids):
241
280
  rql_query = (
242
281
  f"and(in(id,({','.join(ids)})),eq(status,Active))"
@@ -257,9 +296,8 @@ def get_all_agreements(
257
296
 
258
297
 
259
298
  @wrap_mpt_http_error
260
- def get_authorizations_by_currency_and_seller_id(
261
- mpt_client, product_id, currency, owner_id
262
- ):
299
+ def get_authorizations_by_currency_and_seller_id(mpt_client, product_id, currency, owner_id):
300
+ """Retrieve authorizations by product ID, currency, and owner ID."""
263
301
  authorization_filter = (
264
302
  f"eq(product.id,{product_id})&eq(currency,{currency})&eq(owner.id,{owner_id})"
265
303
  )
@@ -328,9 +366,8 @@ def get_listing_by_id(mpt_client, listing_id):
328
366
 
329
367
 
330
368
  @wrap_mpt_http_error
331
- def get_agreement_subscription_by_external_id(
332
- mpt_client, agreement_id, subscription_external_id
333
- ):
369
+ def get_agreement_subscription_by_external_id(mpt_client, agreement_id, subscription_external_id):
370
+ """Retrieve an agreement subscription by external ID."""
334
371
  response = mpt_client.get(
335
372
  f"/commerce/subscriptions?eq(externalIds.vendor,{subscription_external_id})"
336
373
  f"&eq(agreement.id,{agreement_id})"
@@ -359,15 +396,14 @@ def get_agreements_by_external_id_values(mpt_client, external_id, display_values
359
396
 
360
397
 
361
398
  @wrap_mpt_http_error
362
- def get_agreements_by_customer_deployments(
363
- mpt_client, deployment_id_parameter, deployment_ids
364
- ):
399
+ def get_agreements_by_customer_deployments(mpt_client, deployment_id_parameter, deployment_ids):
400
+ """Retrieve agreements by customer deployments."""
365
401
  deployments_list = ",".join(deployment_ids)
366
402
  rql_query = (
367
403
  f"any(parameters.fulfillment,and("
368
404
  f"eq(externalId,{deployment_id_parameter}),"
369
405
  f"in(displayValue,({deployments_list}))))"
370
- f"&select=lines,parameters,subscriptions,product,listing"
406
+ f"&select=lines,parameters,subscriptions,subscriptions.parameters,product,listing"
371
407
  )
372
408
 
373
409
  url = f"/commerce/agreements?{rql_query}"
@@ -391,25 +427,12 @@ def notify(
391
427
  subject: str,
392
428
  message_body: str,
393
429
  limit: int = 1000,
394
- ):
430
+ ) -> None:
395
431
  """
396
432
  Sends notifications to multiple recipients in batches for a specific buyer and
397
433
  category through the MPTClient service. The function retrieves recipients,
398
434
  groups them into manageable batches, and sends notifications using the provided
399
435
  message details.
400
-
401
- Args:
402
- mpt_client (MPTClient): Client object for interacting with MPT service.
403
- category_id (str): Identifier for the category of recipients or messages.
404
- account_id (str): Identifier for the associated account.
405
- buyer_id (str): Identifier for the buyer related to the notification.
406
- subject (str): Subject/title of the notification to be sent.
407
- message_body (str): Content/body of the notification message.
408
- limit (int): Maximum number of recipients to process per batch. Defaults
409
- to 1000.
410
-
411
- Returns:
412
- None
413
436
  """
414
437
  recipients = _paginated(
415
438
  mpt_client,
@@ -433,3 +456,21 @@ def notify(
433
456
  },
434
457
  )
435
458
  response.raise_for_status()
459
+
460
+
461
+ @wrap_mpt_http_error
462
+ def terminate_subscription(mpt_client: MPTClient, subscription_id: str, reason: str) -> dict:
463
+ """
464
+ Terminates a subscription by calling the MPT API.
465
+
466
+ Raises:
467
+ HTTPError: If the HTTP request fails, an HTTPError is raised with
468
+ information about the issue.
469
+ """
470
+ response = mpt_client.post(
471
+ f"/commerce/subscriptions/{subscription_id}/terminate",
472
+ json={"description": reason},
473
+ )
474
+ response.raise_for_status()
475
+
476
+ return response.json()
@@ -1,21 +1,34 @@
1
1
  from contextlib import nullcontext
2
2
 
3
3
  import click
4
+ from django.core.management import execute_from_command_line
4
5
  from opentelemetry import trace
5
6
 
7
+ from mpt_extension_sdk.constants import (
8
+ DEFAULT_APP_CONFIG_GROUP,
9
+ DEFAULT_APP_CONFIG_NAME,
10
+ DJANGO_SETTINGS_MODULE,
11
+ )
12
+ from mpt_extension_sdk.runtime.utils import initialize_extension
13
+
6
14
 
7
15
  @click.command(
8
16
  add_help_option=False, context_settings=dict(ignore_unknown_options=True)
9
17
  )
10
18
  @click.argument("management_args", nargs=-1, type=click.UNPROCESSED)
11
19
  @click.pass_context
12
- def django(ctx, management_args):
20
+ def django(ctx, management_args): # pragma: no cover
13
21
  "Execute Django subcommands."
14
- from mpt_extension_sdk.runtime.initializer import initialize
15
22
 
16
- initialize({})
17
23
  from django.conf import settings
18
- from django.core.management import execute_from_command_line
24
+
25
+ options = {
26
+ "group": DEFAULT_APP_CONFIG_GROUP,
27
+ "name": DEFAULT_APP_CONFIG_NAME,
28
+ "django_settings_module": DJANGO_SETTINGS_MODULE,
29
+ }
30
+
31
+ initialize_extension(options=options)
19
32
 
20
33
  if settings.USE_APPLICATIONINSIGHTS:
21
34
  tracer = trace.get_tracer(__name__)
@@ -1,4 +1,6 @@
1
1
  import click
2
+ import debugpy
3
+ from django.conf import settings
2
4
 
3
5
  from mpt_extension_sdk.runtime.master import Master
4
6
 
@@ -25,17 +27,18 @@ def run(component, color, debug, reload, debug_py):
25
27
  """
26
28
 
27
29
  if debug_py:
28
- import debugpy
29
-
30
30
  host, port = debug_py.split(":")
31
- debugpy.listen((host, int(port)))
31
+ debugpy.listen((host, int(port))) # pragma: no cover
32
+
33
+ options = {
34
+ "color": color,
35
+ "debug": debug,
36
+ "reload": reload,
37
+ "component": component,
38
+ }
32
39
 
33
40
  master = Master(
34
- {
35
- "color": color,
36
- "debug": debug,
37
- "reload": reload,
38
- "component": component,
39
- },
41
+ options,
42
+ settings=settings,
40
43
  )
41
- master.run()
44
+ master.run() # pragma: no cover
@@ -10,7 +10,6 @@ For the full list of settings and their values, see
10
10
  https://docs.djangoproject.com/en/4.2/ref/settings/
11
11
  """
12
12
 
13
- import json
14
13
  import os
15
14
  from pathlib import Path
16
15
 
@@ -26,6 +25,12 @@ BASE_DIR = Path(__file__).resolve().parent.parent
26
25
  # Quick-start development settings - unsuitable for production
27
26
  # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
28
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
+
29
34
  # SECURITY WARNING: don't run with debug turned on in production!
30
35
  DEBUG = True
31
36
 
@@ -128,7 +133,7 @@ APPLICATIONINSIGHTS_CONNECTION_STRING = os.getenv(
128
133
  USE_APPLICATIONINSIGHTS = APPLICATIONINSIGHTS_CONNECTION_STRING != ""
129
134
 
130
135
 
131
- if USE_APPLICATIONINSIGHTS:
136
+ if USE_APPLICATIONINSIGHTS: # pragma: no cover
132
137
  logger_provider = LoggerProvider()
133
138
  set_logger_provider(logger_provider)
134
139
  exporter = AzureMonitorLogExporter(
@@ -141,7 +146,7 @@ LOGGING = {
141
146
  "disable_existing_loggers": False,
142
147
  "formatters": {
143
148
  "verbose": {
144
- "format": "{asctime} {name} {levelname} (pid: {process}) {message}",
149
+ "format": "{asctime} {name} {levelname} (pid: {process}, thread: {thread}) {message}",
145
150
  "style": "{",
146
151
  },
147
152
  "rich": {
@@ -149,7 +154,7 @@ LOGGING = {
149
154
  "style": "{",
150
155
  },
151
156
  "opentelemetry": {
152
- "format": "(pid: {process}) {message}",
157
+ "format": "(pid: {process}, thread: {thread}) {message}",
153
158
  "style": "{",
154
159
  },
155
160
  },
@@ -203,16 +208,18 @@ MPT_API_TOKEN = os.getenv("MPT_API_TOKEN", "change-me!")
203
208
  MPT_API_TOKEN_OPERATIONS = os.getenv("MPT_API_TOKEN_OPERATIONS", "change-me!")
204
209
  MPT_PRODUCTS_IDS = os.getenv("MPT_PRODUCTS_IDS", "PRD-1111-1111")
205
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")
206
212
 
207
213
  MPT_ORDERS_API_POLLING_INTERVAL_SECS = int(
208
214
  os.getenv("MPT_ORDERS_API_POLLING_INTERVAL_SECS", "120")
209
215
  )
210
216
 
211
- # TODO: Should be synced with the initializer.py::initialize function
212
- MPT_NOTIFY_CATEGORIES = json.loads(
213
- os.getenv("MPT_NOTIFY_CATEGORIES", '{"ORDERS": "NTC-0000-0006"}')
214
- )
215
-
216
217
  EXTENSION_CONFIG = {
217
218
  "DUE_DATE_DAYS": "30",
219
+ "ORDER_CREATION_WINDOW_HOURS": os.getenv("EXT_ORDER_CREATION_WINDOW_HOURS", "24"),
218
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)
@@ -1,11 +1,13 @@
1
- import signal
2
- from threading import Event
1
+ import signal # pragma: no cover
2
+ from threading import Event # pragma: no cover
3
3
 
4
- from django.core.management.base import BaseCommand
4
+ from django.core.management.base import BaseCommand # pragma: no cover
5
5
 
6
- from mpt_extension_sdk.constants import CONSUME_EVENTS_HELP_TEXT
7
- from mpt_extension_sdk.runtime.events.dispatcher import Dispatcher
8
- from mpt_extension_sdk.runtime.events.producers import OrderEventProducer
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
+ )
9
11
 
10
12
 
11
13
  class Command(BaseCommand): # pragma: no cover
@@ -5,6 +5,10 @@ import time
5
5
  from collections import deque
6
6
  from concurrent.futures import ThreadPoolExecutor
7
7
 
8
+ from mpt_extension_sdk.constants import (
9
+ DEFAULT_APP_CONFIG_GROUP,
10
+ DEFAULT_APP_CONFIG_NAME,
11
+ )
8
12
  from mpt_extension_sdk.core.events.dataclasses import Event
9
13
  from mpt_extension_sdk.core.events.registry import EventsRegistry
10
14
  from mpt_extension_sdk.core.utils import setup_client
@@ -24,8 +28,8 @@ def done_callback(futures, key, future): # pragma: no cover
24
28
 
25
29
 
26
30
  class Dispatcher:
27
- def __init__(self):
28
- self.registry: EventsRegistry = get_events_registry()
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)
29
33
  self.queue = deque()
30
34
  self.futures = {}
31
35
  self.executor = ThreadPoolExecutor()
@@ -45,7 +49,7 @@ class Dispatcher:
45
49
  def running(self):
46
50
  return self.running_event.is_set()
47
51
 
48
- def dispatch_event(self, event: Event):
52
+ def dispatch_event(self, event: Event): # pragma: no cover
49
53
  if self.registry.is_event_supported(event.type):
50
54
  logger.info(f"event of type {event.type} with id {event.id} accepted")
51
55
  self.queue.appendleft((event.type, event))
@@ -78,7 +78,7 @@ class OrderEventProducer(EventProducer):
78
78
  rql_query = f"and(in(agreement.product.id,({products})),eq(status,processing))"
79
79
  url = (
80
80
  f"/commerce/orders?{rql_query}&select=audit,parameters,lines,subscriptions,"
81
- f"subscriptions.lines,agreement,buyer&order=audit.created.at"
81
+ f"subscriptions.lines,agreement,buyer,seller&order=audit.created.at"
82
82
  )
83
83
  page = None
84
84
  limit = 10
@@ -24,10 +24,8 @@ JSON_EXT_VARIABLES = {
24
24
  }
25
25
 
26
26
 
27
- def initialize(options):
27
+ def initialize(options, group=DEFAULT_APP_CONFIG_GROUP, name=DEFAULT_APP_CONFIG_NAME):
28
28
  rich.reconfigure(theme=Theme({"repr.mpt_id": "bold light_salmon3"}))
29
- group = options.get("app_config_group", DEFAULT_APP_CONFIG_GROUP)
30
- name = options.get("app_config_name", DEFAULT_APP_CONFIG_NAME)
31
29
  django_settings_module = options.get(
32
30
  "django_settings_module", DJANGO_SETTINGS_MODULE
33
31
  )
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ import os
2
3
  import signal
3
4
  import threading
4
5
  import time
@@ -14,20 +15,21 @@ logger = logging.getLogger(__name__)
14
15
 
15
16
 
16
17
  HANDLED_SIGNALS = (signal.SIGINT, signal.SIGTERM)
17
- PROCESS_CHECK_INTERVAL_SECS = 5
18
+ PROCESS_CHECK_INTERVAL_SECS = int(os.environ.get("PROCESS_CHECK_INTERVAL_SECS", 5))
18
19
 
19
20
 
20
- def _display_path(path):
21
+ def _display_path(path): # pragma: no cover
21
22
  try:
22
23
  return f'"{path.relative_to(Path.cwd())}"'
23
- except ValueError: # pragma: no cover
24
+ except ValueError:
24
25
  return f'"{path}"'
25
26
 
26
27
 
27
28
  class Master:
28
- def __init__(self, options):
29
+ def __init__(self, options, settings):
29
30
  self.workers = {}
30
31
  self.options = options
32
+ self.settings = settings
31
33
  self.stop_event = threading.Event()
32
34
  self.monitor_event = threading.Event()
33
35
  self.watch_filter = PythonFilter(ignore_paths=None)
@@ -51,9 +53,7 @@ class Master:
51
53
  "gunicorn": start_gunicorn,
52
54
  }
53
55
  case "consumer":
54
- self.proc_targets = {
55
- "event-consumer": start_event_consumer,
56
- }
56
+ self.proc_targets = {"event-consumer": start_event_consumer}
57
57
  case _:
58
58
  self.proc_targets = {
59
59
  "event-consumer": start_event_consumer,
@@ -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
@@ -4,6 +4,9 @@ import sys
4
4
  from importlib.metadata import entry_points
5
5
 
6
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
7
10
  from pyfiglet import Figlet
8
11
  from rich.console import Console
9
12
  from rich.text import Text
@@ -85,10 +88,9 @@ def show_banner(): # pragma: no cover
85
88
 
86
89
  for line in banner_lines:
87
90
  colored_line = Text()
88
- for i in range(len(line)):
89
- char = line[i : i + 1]
90
- char.stylize(colors[i])
91
- colored_line = Text.assemble(colored_line, char)
91
+ for idx, line_char in enumerate(line):
92
+ line_char.stylize(colors[idx])
93
+ colored_line = Text.assemble(colored_line, line_char)
92
94
  console.print(colored_line)
93
95
 
94
96
 
@@ -105,3 +107,42 @@ def get_extension_variables(json_ext_variables):
105
107
 
106
108
  variables[var[0][4:]] = value
107
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)
@@ -2,7 +2,11 @@ from django.core.management import call_command
2
2
  from django.core.wsgi import get_wsgi_application
3
3
  from gunicorn.app.base import BaseApplication
4
4
 
5
- from mpt_extension_sdk.runtime.initializer import initialize
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
6
10
 
7
11
 
8
12
  class ExtensionWebApplication(BaseApplication):
@@ -25,19 +29,24 @@ class ExtensionWebApplication(BaseApplication):
25
29
 
26
30
 
27
31
  def start_event_consumer(options):
28
- initialize(options)
32
+ initialize_extension(options)
29
33
  call_command("consume_events")
30
34
 
31
35
 
32
- def start_gunicorn(options):
33
- initialize(options)
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)
34
42
 
35
43
  logging_config = {
36
44
  "version": 1,
37
45
  "disable_existing_loggers": False,
38
46
  "formatters": {
39
47
  "verbose": {
40
- "format": "{asctime} {name} {levelname} (pid: {process}) {message}",
48
+ "format":
49
+ "{asctime} {name} {levelname} (pid: {process}, thread: {thread}) {message}",
41
50
  "style": "{",
42
51
  },
43
52
  "rich": {
@@ -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)
@@ -0,0 +1,392 @@
1
+ from mpt_extension_sdk.swo_rql import constants
2
+
3
+
4
+ def parse_kwargs(query_dict):
5
+ query = []
6
+ for lookup, value in query_dict.items():
7
+ tokens = lookup.split("__")
8
+ if len(tokens) == 1:
9
+ # field=value
10
+ field = tokens[0]
11
+ value = rql_encode("eq", value)
12
+ query.append(f"eq({field},{value})")
13
+ continue
14
+ op = tokens[-1]
15
+ if op not in constants.KEYWORDS:
16
+ # field__nested=value
17
+ field = ".".join(tokens)
18
+ value = rql_encode("eq", value)
19
+ query.append(f"eq({field},{value})")
20
+ continue
21
+ field = ".".join(tokens[:-1])
22
+ if op in constants.COMP or op in constants.SEARCH:
23
+ value = rql_encode(op, value)
24
+ query.append(f"{op}({field},{value})")
25
+ continue
26
+ if op in constants.LIST:
27
+ value = rql_encode(op, value)
28
+ query.append(f"{op}({field},({value}))")
29
+ continue
30
+
31
+ cmpop = "eq" if value is True else "ne"
32
+ expr = "null()" if op == constants.NULL else "empty()"
33
+ query.append(f"{cmpop}({field},{expr})")
34
+
35
+ return query
36
+
37
+
38
+ def rql_encode(op, value):
39
+ from datetime import date, datetime
40
+ from decimal import Decimal
41
+
42
+ if op not in constants.LIST:
43
+ if isinstance(value, str):
44
+ return value
45
+ if isinstance(value, bool):
46
+ return "true" if value else "false"
47
+ if isinstance(value, int | float | Decimal):
48
+ return str(value)
49
+ if isinstance(value, date | datetime):
50
+ return value.isoformat()
51
+ if op in constants.LIST and isinstance(value, list | tuple):
52
+ return ",".join(value)
53
+ raise TypeError(f"the `{op}` operator doesn't support the {type(value)} type.")
54
+
55
+
56
+ class RQLQuery:
57
+ """
58
+ Helper class to construct complex RQL queries.
59
+
60
+ Usage:
61
+
62
+ ```py3
63
+ rql = R(field='value', field2__in=('v1', 'v2'), field3__empty=True)
64
+ ```
65
+ !!! note
66
+ All the lookups expressed as keyword arguments are combined together with a logical `and`.
67
+
68
+
69
+ Using the ``n`` method:
70
+
71
+ ```py3
72
+ rql = (
73
+ R().n('field').eq('value')
74
+ & R().n('field2').anyof(('v1', 'v2'))
75
+ & R().n('field3').empty(True)
76
+ )
77
+ ```
78
+
79
+ The previous query can be expressed in a more concise form like:
80
+
81
+ ```py3
82
+ rql = R().field.eq('value') & R().field2.anyof(('v1', 'v2')) & r.field3.empty(True)
83
+ ```
84
+
85
+ ```py3
86
+ rql = R("field").eq("value")
87
+ ```
88
+
89
+ The R object support the bitwise operators `&`, `|` and `~`.
90
+
91
+ Nested fields can be expressed using dot notation:
92
+
93
+ ```py3
94
+ rql = R().n('nested.field').eq('value')
95
+ ```
96
+
97
+ or
98
+
99
+ ```py3
100
+ rql = R().nested.field.eq('value')
101
+ ```
102
+ """
103
+
104
+ AND = "and"
105
+ OR = "or"
106
+ EXPRESSION = "expr"
107
+
108
+ def __init__(
109
+ self,
110
+ _field=None,
111
+ *,
112
+ _op=EXPRESSION,
113
+ _children=None,
114
+ _negated=False,
115
+ _expr=None,
116
+ **kwargs,
117
+ ):
118
+ self.op = _op
119
+ self.children = _children or []
120
+ self.negated = _negated
121
+ self.expr = _expr
122
+ self._path = []
123
+ self._field = None
124
+ if _field:
125
+ self.n(_field)
126
+ if len(kwargs) == 1:
127
+ self.op = self.EXPRESSION
128
+ self.expr = parse_kwargs(kwargs)[0]
129
+ if len(kwargs) > 1:
130
+ self.op = self.AND
131
+ for token in parse_kwargs(kwargs):
132
+ self.children.append(RQLQuery(_expr=token))
133
+
134
+ def __len__(self):
135
+ if self.op == self.EXPRESSION:
136
+ if self.expr:
137
+ return 1
138
+ return 0
139
+ return len(self.children)
140
+
141
+ def __bool__(self):
142
+ return bool(self.children) or bool(self.expr)
143
+
144
+ def __eq__(self, other):
145
+ return (
146
+ self.op == other.op
147
+ and self.children == other.children
148
+ and self.negated == other.negated
149
+ and self.expr == other.expr
150
+ )
151
+
152
+ def __hash__(self):
153
+ return hash(
154
+ (
155
+ self.op,
156
+ self.expr,
157
+ self.negated,
158
+ *(hash(value) for value in self.children),
159
+ ),
160
+ )
161
+
162
+ def __repr__(self):
163
+ if self.op == self.EXPRESSION:
164
+ return f"<R({self.op}) {self.expr}>"
165
+ return f"<R({self.op})>"
166
+
167
+ def __and__(self, other):
168
+ return self._join(other, self.AND)
169
+
170
+ def __or__(self, other):
171
+ return self._join(other, self.OR)
172
+
173
+ def __invert__(self):
174
+ query = RQLQuery(_op=self.AND, _expr=self.expr, _negated=True)
175
+ query._append(self)
176
+ return query
177
+
178
+ def __getattr__(self, name):
179
+ return self.n(name)
180
+
181
+ def __str__(self):
182
+ return self._to_string(self)
183
+
184
+ def n(self, name):
185
+ """
186
+ Set the current field for this `R` object.
187
+
188
+ Args:
189
+ name (str): Name of the field.
190
+ """
191
+ if self._field:
192
+ raise AttributeError("Already evaluated")
193
+
194
+ self._path.extend(name.split("."))
195
+ return self
196
+
197
+ def ne(self, value):
198
+ """
199
+ Apply the `ne` operator to the field this `R` object refers to.
200
+
201
+ Args:
202
+ value (str): The value to which compare the field.
203
+ """
204
+ return self._bin("ne", value)
205
+
206
+ def eq(self, value):
207
+ """
208
+ Apply the `eq` operator to the field this `R` object refers to.
209
+
210
+ Args:
211
+ value (str): The value to which compare the field.
212
+ """
213
+ return self._bin("eq", value)
214
+
215
+ def lt(self, value):
216
+ """
217
+ Apply the `lt` operator to the field this `R` object refers to.
218
+
219
+ Args:
220
+ value (str): The value to which compare the field.
221
+ """
222
+ return self._bin("lt", value)
223
+
224
+ def le(self, value):
225
+ """
226
+ Apply the `le` operator to the field this `R` object refers to.
227
+
228
+ Args:
229
+ value (str): The value to which compare the field.
230
+ """
231
+ return self._bin("le", value)
232
+
233
+ def gt(self, value):
234
+ """
235
+ Apply the `gt` operator to the field this `R` object refers to.
236
+
237
+ Args:
238
+ value (str): The value to which compare the field.
239
+ """
240
+ return self._bin("gt", value)
241
+
242
+ def ge(self, value):
243
+ """
244
+ Apply the `ge` operator to the field this `R` object refers to.
245
+
246
+ Args:
247
+ value (str): The value to which compare the field.
248
+ """
249
+ return self._bin("ge", value)
250
+
251
+ def out(self, value: list[str]):
252
+ """
253
+ Apply the `out` operator to the field this `R` object refers to.
254
+
255
+ Args:
256
+ value (list[str]): The list of values to which compare the field.
257
+ """
258
+ return self._list("out", value)
259
+
260
+ def in_(self, value):
261
+ return self._list("in", value)
262
+
263
+ def oneof(self, value: list[str]):
264
+ """
265
+ Apply the `in` operator to the field this `R` object refers to.
266
+
267
+ Args:
268
+ value (list[str]): The list of values to which compare the field.
269
+ """
270
+ return self._list("in", value)
271
+
272
+ def null(self, value: list[str]):
273
+ """
274
+ Apply the `null` operator to the field this `R` object refers to.
275
+
276
+ Args:
277
+ value (list[str]): The value to which compare the field.
278
+ """
279
+ return self._bool("null", value)
280
+
281
+ def empty(self, value: bool = True):
282
+ """
283
+ Apply the `empty` operator to the field this `R` object refers to.
284
+
285
+ Usage: `R().field.empty()
286
+
287
+ For not empty: `R().field.empty(False)` or `R().field.not_empty()`
288
+ """
289
+ return self._bool("empty", value)
290
+
291
+ def not_empty(self):
292
+ """
293
+ Apply the `not_empty` operator to the field this `R` object refers to.
294
+ """
295
+ query = self._bool("empty", False)
296
+ return query
297
+
298
+ def like(self, value: list[str]):
299
+ """
300
+ Apply the `like` operator to the field this `R` object refers to.
301
+
302
+ Args:
303
+ value (list[str]): The value to which compare the field.
304
+ """
305
+ return self._bin("like", value)
306
+
307
+ def ilike(self, value: list[str]):
308
+ """
309
+ Apply the `ilike` operator to the field this `R` object refers to.
310
+
311
+ Args:
312
+ value (list[str]): The value to which compare the field.
313
+ """
314
+ return self._bin("ilike", value)
315
+
316
+ def _bin(self, op, value):
317
+ self._field = ".".join(self._path)
318
+ value = rql_encode(op, value)
319
+ self.expr = f"{op}({self._field},{value})"
320
+ return self
321
+
322
+ def _list(self, op, value):
323
+ self._field = ".".join(self._path)
324
+ value = rql_encode(op, value)
325
+ self.expr = f"{op}({self._field},({value}))"
326
+ return self
327
+
328
+ def _bool(self, expr, value):
329
+ self._field = ".".join(self._path)
330
+ if bool(value) is False:
331
+ self.expr = f"ne({self._field},{expr}())"
332
+ return self
333
+ self.expr = f"eq({self._field},{expr}())"
334
+ return self
335
+
336
+ def _to_string(self, query):
337
+ tokens = []
338
+ if query.expr:
339
+ if query.negated:
340
+ return f"not({query.expr})"
341
+ return query.expr
342
+ for c in query.children:
343
+ if c.expr:
344
+ if c.negated:
345
+ tokens.append(f"not({c.expr})")
346
+ else:
347
+ tokens.append(c.expr)
348
+ continue
349
+ tokens.append(self._to_string(c))
350
+
351
+ if not tokens:
352
+ return ""
353
+
354
+ if query.negated:
355
+ return f'not({query.op}({",".join(tokens)}))'
356
+ return f'{query.op}({",".join(tokens)})'
357
+
358
+ def _copy(self, other):
359
+ return RQLQuery(
360
+ _op=other.op,
361
+ _children=other.children[:],
362
+ _expr=other.expr,
363
+ )
364
+
365
+ def _join(self, other, op):
366
+ if self == other:
367
+ return self._copy(self)
368
+ if not other:
369
+ return self._copy(self)
370
+ if not self:
371
+ return self._copy(other)
372
+
373
+ query = RQLQuery(_op=op)
374
+ query._append(self)
375
+ query._append(other)
376
+ return query
377
+
378
+ def _append(self, other):
379
+ if other in self.children:
380
+ return other
381
+
382
+ if (
383
+ other.op == self.op or (len(other) == 1 and other.op != self.EXPRESSION)
384
+ ) and not other.negated:
385
+ self.children.extend(other.children)
386
+ return self
387
+
388
+ self.children.append(other)
389
+ return self
390
+
391
+
392
+ R = RQLQuery
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "mpt-extension-sdk"
3
- version = "4.3.4"
3
+ version = "4.4.0"
4
4
  description = "Extensions SDK for SoftwareONE Marketplace Platform"
5
5
  authors = ["SoftwareOne AG"]
6
6
  readme = "README.md"
@@ -56,7 +56,7 @@ types-openpyxl = "3.1.*"
56
56
  types-requests = "2.31.*"
57
57
 
58
58
  [tool.poetry.scripts]
59
- swosdk = 'mpt_extension_sdk.runtime.swoext:main'
59
+ swoext = 'mpt_extension_sdk.runtime.swoext:main'
60
60
 
61
61
 
62
62
  [tool.poetry.plugins."swo.mpt.sdk"]