mpt-extension-sdk 4.3.4__tar.gz → 4.5.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.5.0}/PKG-INFO +4 -2
  2. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.0}/mpt_extension_sdk/constants.py +5 -1
  3. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.0}/mpt_extension_sdk/mpt_http/mpt.py +151 -56
  4. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.0}/mpt_extension_sdk/runtime/commands/django.py +17 -4
  5. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.0}/mpt_extension_sdk/runtime/commands/run.py +13 -10
  6. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.0}/mpt_extension_sdk/runtime/djapp/conf/default.py +16 -9
  7. mpt_extension_sdk-4.5.0/mpt_extension_sdk/runtime/djapp/conf/urls.py +9 -0
  8. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.0}/mpt_extension_sdk/runtime/djapp/management/commands/consume_events.py +8 -6
  9. mpt_extension_sdk-4.5.0/mpt_extension_sdk/runtime/events/__init__.py +0 -0
  10. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.0}/mpt_extension_sdk/runtime/events/dispatcher.py +7 -3
  11. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.0}/mpt_extension_sdk/runtime/events/producers.py +1 -1
  12. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.0}/mpt_extension_sdk/runtime/initializer.py +1 -3
  13. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.0}/mpt_extension_sdk/runtime/master.py +7 -7
  14. mpt_extension_sdk-4.5.0/mpt_extension_sdk/runtime/tracer.py +18 -0
  15. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.0}/mpt_extension_sdk/runtime/utils.py +45 -4
  16. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.0}/mpt_extension_sdk/runtime/workers.py +14 -5
  17. mpt_extension_sdk-4.5.0/mpt_extension_sdk/swo_rql/__init__.py +5 -0
  18. mpt_extension_sdk-4.5.0/mpt_extension_sdk/swo_rql/constants.py +7 -0
  19. mpt_extension_sdk-4.5.0/mpt_extension_sdk/swo_rql/query_builder.py +392 -0
  20. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.0}/pyproject.toml +2 -2
  21. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.0}/LICENSE +0 -0
  22. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.0}/README.md +0 -0
  23. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.0}/mpt_extension_sdk/__init__.py +0 -0
  24. {mpt_extension_sdk-4.3.4/mpt_extension_sdk/core → mpt_extension_sdk-4.5.0/mpt_extension_sdk/airtable}/__init__.py +0 -0
  25. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.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.5.0/mpt_extension_sdk/core}/__init__.py +0 -0
  27. {mpt_extension_sdk-4.3.4/mpt_extension_sdk/flows → mpt_extension_sdk-4.5.0/mpt_extension_sdk/core/events}/__init__.py +0 -0
  28. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.0}/mpt_extension_sdk/core/events/dataclasses.py +0 -0
  29. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.0}/mpt_extension_sdk/core/events/registry.py +0 -0
  30. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.0}/mpt_extension_sdk/core/extension.py +0 -0
  31. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.0}/mpt_extension_sdk/core/security.py +0 -0
  32. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.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.5.0/mpt_extension_sdk/flows}/__init__.py +0 -0
  34. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.0}/mpt_extension_sdk/flows/context.py +0 -0
  35. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.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.5.0/mpt_extension_sdk/key_vault}/__init__.py +0 -0
  37. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.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.5.0/mpt_extension_sdk/mpt_http}/__init__.py +0 -0
  39. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.0}/mpt_extension_sdk/mpt_http/base.py +0 -0
  40. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.0}/mpt_extension_sdk/mpt_http/utils.py +0 -0
  41. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.0}/mpt_extension_sdk/mpt_http/wrap_http_error.py +0 -0
  42. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.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.5.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.5.0/mpt_extension_sdk/runtime/djapp}/__init__.py +0 -0
  45. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.0}/mpt_extension_sdk/runtime/djapp/apps.py +0 -0
  46. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.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.5.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.5.0/mpt_extension_sdk/runtime/djapp/management/commands}/__init__.py +0 -0
  49. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.0}/mpt_extension_sdk/runtime/djapp/middleware.py +0 -0
  50. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.0}/mpt_extension_sdk/runtime/events/utils.py +0 -0
  51. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.0}/mpt_extension_sdk/runtime/logging.py +0 -0
  52. {mpt_extension_sdk-4.3.4 → mpt_extension_sdk-4.5.0}/mpt_extension_sdk/runtime/swoext.py +0 -0
@@ -1,14 +1,16 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: mpt-extension-sdk
3
- Version: 4.3.4
3
+ Version: 4.5.0
4
4
  Summary: Extensions SDK for SoftwareONE Marketplace Platform
5
5
  License: Apache-2.0
6
+ License-File: LICENSE
6
7
  Author: SoftwareOne AG
7
8
  Requires-Python: >=3.12,<4
8
9
  Classifier: License :: OSI Approved :: Apache Software License
9
10
  Classifier: Programming Language :: Python :: 3
10
11
  Classifier: Programming Language :: Python :: 3.12
11
12
  Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
12
14
  Requires-Dist: azure-identity (>=1.21.0,<2.0.0)
13
15
  Requires-Dist: azure-keyvault-secrets (>=4.9.0,<5.0.0)
14
16
  Requires-Dist: azure-monitor-opentelemetry-exporter (==1.0.0b25)
@@ -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,74 @@ 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, asset):
106
+ """Create a new asset."""
107
+ response = mpt_client.post("/commerce/assets", json=asset)
108
+ response.raise_for_status()
109
+ return response.json()
110
+
111
+
112
+ @wrap_mpt_http_error
113
+ def create_order_asset(mpt_client, order_id, asset):
114
+ """Create a new asset for an order."""
115
+ response = mpt_client.post(f"/commerce/orders/{order_id}/assets", json=asset)
116
+ response.raise_for_status()
117
+ return response.json()
118
+
119
+
120
+ @wrap_mpt_http_error
121
+ def update_asset(mpt_client, asset_id, **kwargs):
122
+ """Update an asset."""
123
+ response = mpt_client.put(f"/commerce/assets/{asset_id}", json=kwargs)
124
+ response.raise_for_status()
125
+ return response.json()
126
+
127
+
128
+ @wrap_mpt_http_error
129
+ def update_order_asset(mpt_client, order_id, asset_id, **kwargs):
130
+ """Update an order asset."""
131
+ response = mpt_client.put(f"/commerce/orders/{order_id}/assets/{asset_id}", json=kwargs)
132
+ response.raise_for_status()
133
+ return response.json()
134
+
135
+
136
+ @wrap_mpt_http_error
137
+ def get_agreement_asset_by_external_id(mpt_client, agreement_id, asset_external_id):
138
+ """Retrieve an agreement asset by external ID."""
139
+ response = mpt_client.get(
140
+ f"/commerce/assets?eq(externalIds.vendor,{asset_external_id})"
141
+ f"&eq(agreement.id,{agreement_id})"
142
+ f"&eq(status,Active)"
143
+ f"&select=agreement.id&limit=1"
144
+ )
145
+ response.raise_for_status()
146
+ assets = response.json()
147
+ return assets["data"][0] if assets["data"] else None
148
+
149
+
150
+ @wrap_mpt_http_error
151
+ def get_asset_by_id(mpt_client, asset_id):
152
+ """Get an asset by ID."""
153
+ response = mpt_client.get(f"/commerce/assets/{asset_id}")
154
+ response.raise_for_status()
155
+ return response.json()
156
+
157
+
158
+ @wrap_mpt_http_error
159
+ def get_order_asset_by_external_id(mpt_client, order_id, asset_external_id):
160
+ """Retrieve an order asset by its external ID."""
161
+ response = mpt_client.get(
162
+ f"/commerce/orders/{order_id}/assets?eq(externalIds.vendor,{asset_external_id})&limit=1",
163
+ )
164
+ response.raise_for_status()
165
+ assets = response.json()
166
+ if assets["$meta"]["pagination"]["total"] == 1:
167
+ return assets["data"][0]
168
+
169
+ return None
170
+
171
+
107
172
  @wrap_mpt_http_error
108
173
  def create_subscription(mpt_client, order_id, subscription):
109
174
  response = mpt_client.post(
@@ -125,9 +190,8 @@ def update_subscription(mpt_client, order_id, subscription_id, **kwargs):
125
190
 
126
191
 
127
192
  @wrap_mpt_http_error
128
- def get_order_subscription_by_external_id(
129
- mpt_client, order_id, subscription_external_id
130
- ):
193
+ def get_order_subscription_by_external_id(mpt_client, order_id, subscription_external_id):
194
+ """Retrieve an order subscription by its external ID."""
131
195
  response = mpt_client.get(
132
196
  f"/commerce/orders/{order_id}/subscriptions?eq(externalIds.vendor,{subscription_external_id})&limit=1",
133
197
  )
@@ -139,9 +203,9 @@ def get_order_subscription_by_external_id(
139
203
 
140
204
  @wrap_mpt_http_error
141
205
  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
- )
206
+ """Retrieve product items by their SKUs."""
207
+ skus_str = ",".join(skus)
208
+ rql_query = f"and(eq(product.id,{product_id}),in(externalIds.vendor,({skus_str})))"
145
209
  url = f"/catalog/items?{rql_query}"
146
210
  return _paginated(mpt_client, url)
147
211
 
@@ -168,6 +232,14 @@ def get_product_template_or_default(mpt_client, product_id, status, name=None):
168
232
  return templates["data"][0]
169
233
 
170
234
 
235
+ def get_template_by_name(mpt_client, product_id, template_name):
236
+ url = f"/catalog/products/{product_id}/templates?eq(name,{template_name})"
237
+ response = mpt_client.get(url)
238
+ response.raise_for_status()
239
+ templates = response.json()
240
+ return templates["data"][0]
241
+
242
+
171
243
  @wrap_mpt_http_error
172
244
  def update_agreement(mpt_client, agreement_id, **kwargs):
173
245
  response = mpt_client.put(
@@ -184,21 +256,6 @@ def get_agreements_by_query(mpt_client, query):
184
256
  return _paginated(mpt_client, url)
185
257
 
186
258
 
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
259
  @wrap_mpt_http_error
203
260
  def update_agreement_subscription(mpt_client, subscription_id, **kwargs):
204
261
  response = mpt_client.put(
@@ -237,29 +294,64 @@ def get_product_onetime_items_by_ids(mpt_client, product_id, item_ids):
237
294
  return _paginated(mpt_client, url)
238
295
 
239
296
 
297
+ @wrap_mpt_http_error
298
+ def get_product_items_by_period(
299
+ mpt_client,
300
+ product_id: str,
301
+ period: str,
302
+ vendor_external_ids: Iterable[str] | None = None,
303
+ ):
304
+ """
305
+ Fetches product items based on a specified period and filters.
306
+
307
+ Args:
308
+ mpt_client: Client in tance to interact with the required API.
309
+ product_id (str): The unique identifier of the product to fetch items for.
310
+ period (str): The period for which to fetch the product items.
311
+ vendor_external_ids (Iterable[str] | None):
312
+ Optional. A list of vendor external IDs to filter out the product items by. Defaults
313
+ to None.
314
+
315
+ Returns:
316
+ list:
317
+ A paginated list of product items matching the specified criteria.
318
+
319
+ """
320
+ product_cond = f"eq(product.id,{product_id})"
321
+ vendors_cond = ""
322
+ if vendor_external_ids:
323
+ vendor_ids = ",".join(vendor_external_ids)
324
+ vendors_cond = f",in(externalIds.vendor,({vendor_ids})))"
325
+ rql_query = f"and({product_cond},eq(terms.period,{period}){vendors_cond})"
326
+ url = f"/catalog/items?{rql_query}"
327
+
328
+ return _paginated(mpt_client, url)
329
+
330
+
240
331
  def get_agreements_by_ids(mpt_client, ids):
332
+ """Retrieve agreements by their IDs."""
333
+ ids_str = ",".join(ids)
241
334
  rql_query = (
242
- f"and(in(id,({','.join(ids)})),eq(status,Active))"
243
- "&select=lines,parameters,subscriptions,product,listing"
335
+ f"and(in(id,({ids_str})),eq(status,Active))"
336
+ "&select=assets,lines,parameters,subscriptions,product,listing"
244
337
  )
245
338
  return get_agreements_by_query(mpt_client, rql_query)
246
339
 
247
340
 
248
- def get_all_agreements(
249
- mpt_client,
250
- ):
251
- product_condition = f"in(product.id,({','.join(settings.MPT_PRODUCTS_IDS)}))"
341
+ def get_all_agreements(mpt_client):
342
+ """Retrieve all active agreements for specific products."""
343
+ product_ids_str = ",".join(settings.MPT_PRODUCTS_IDS)
344
+ product_condition = f"in(product.id,({product_ids_str}))"
252
345
 
253
346
  return get_agreements_by_query(
254
347
  mpt_client,
255
- f"and(eq(status,Active),{product_condition})&select=lines,parameters,subscriptions,product,listing",
348
+ f"and(eq(status,Active),{product_condition})&select=assets,lines,parameters,subscriptions,product,listing",
256
349
  )
257
350
 
258
351
 
259
352
  @wrap_mpt_http_error
260
- def get_authorizations_by_currency_and_seller_id(
261
- mpt_client, product_id, currency, owner_id
262
- ):
353
+ def get_authorizations_by_currency_and_seller_id(mpt_client, product_id, currency, owner_id):
354
+ """Retrieve authorizations by product ID, currency, and owner ID."""
263
355
  authorization_filter = (
264
356
  f"eq(product.id,{product_id})&eq(currency,{currency})&eq(owner.id,{owner_id})"
265
357
  )
@@ -328,9 +420,8 @@ def get_listing_by_id(mpt_client, listing_id):
328
420
 
329
421
 
330
422
  @wrap_mpt_http_error
331
- def get_agreement_subscription_by_external_id(
332
- mpt_client, agreement_id, subscription_external_id
333
- ):
423
+ def get_agreement_subscription_by_external_id(mpt_client, agreement_id, subscription_external_id):
424
+ """Retrieve an agreement subscription by external ID."""
334
425
  response = mpt_client.get(
335
426
  f"/commerce/subscriptions?eq(externalIds.vendor,{subscription_external_id})"
336
427
  f"&eq(agreement.id,{agreement_id})"
@@ -359,15 +450,14 @@ def get_agreements_by_external_id_values(mpt_client, external_id, display_values
359
450
 
360
451
 
361
452
  @wrap_mpt_http_error
362
- def get_agreements_by_customer_deployments(
363
- mpt_client, deployment_id_parameter, deployment_ids
364
- ):
453
+ def get_agreements_by_customer_deployments(mpt_client, deployment_id_parameter, deployment_ids):
454
+ """Retrieve agreements by customer deployments."""
365
455
  deployments_list = ",".join(deployment_ids)
366
456
  rql_query = (
367
457
  f"any(parameters.fulfillment,and("
368
458
  f"eq(externalId,{deployment_id_parameter}),"
369
459
  f"in(displayValue,({deployments_list}))))"
370
- f"&select=lines,parameters,subscriptions,product,listing"
460
+ f"&select=lines,parameters,subscriptions,subscriptions.parameters,product,listing"
371
461
  )
372
462
 
373
463
  url = f"/commerce/agreements?{rql_query}"
@@ -391,25 +481,12 @@ def notify(
391
481
  subject: str,
392
482
  message_body: str,
393
483
  limit: int = 1000,
394
- ):
484
+ ) -> None:
395
485
  """
396
486
  Sends notifications to multiple recipients in batches for a specific buyer and
397
487
  category through the MPTClient service. The function retrieves recipients,
398
488
  groups them into manageable batches, and sends notifications using the provided
399
489
  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
490
  """
414
491
  recipients = _paginated(
415
492
  mpt_client,
@@ -433,3 +510,21 @@ def notify(
433
510
  },
434
511
  )
435
512
  response.raise_for_status()
513
+
514
+
515
+ @wrap_mpt_http_error
516
+ def terminate_subscription(mpt_client: MPTClient, subscription_id: str, reason: str) -> dict:
517
+ """
518
+ Terminates a subscription by calling the MPT API.
519
+
520
+ Raises:
521
+ HTTPError: If the HTTP request fails, an HTTPError is raised with
522
+ information about the issue.
523
+ """
524
+ response = mpt_client.post(
525
+ f"/commerce/subscriptions/{subscription_id}/terminate",
526
+ json={"description": reason},
527
+ )
528
+ response.raise_for_status()
529
+
530
+ 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.5.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"]