pyxecm 3.6.0__tar.gz → 3.6.2__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 (72) hide show
  1. {pyxecm-3.6.0 → pyxecm-3.6.2}/PKG-INFO +1 -1
  2. {pyxecm-3.6.0 → pyxecm-3.6.2}/pyproject.toml +1 -1
  3. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm/otca.py +60 -12
  4. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm/otcs.py +145 -87
  5. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_customizer/payload.py +143 -0
  6. {pyxecm-3.6.0 → pyxecm-3.6.2}/.gitignore +0 -0
  7. {pyxecm-3.6.0 → pyxecm-3.6.2}/README.md +0 -0
  8. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm/__init__.py +0 -0
  9. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm/avts.py +0 -0
  10. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm/coreshare.py +0 -0
  11. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm/helper/__init__.py +0 -0
  12. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm/helper/assoc.py +0 -0
  13. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm/helper/data.py +0 -0
  14. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm/helper/logadapter.py +0 -0
  15. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm/helper/web.py +0 -0
  16. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm/helper/xml.py +0 -0
  17. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm/otac.py +0 -0
  18. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm/otawp.py +0 -0
  19. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm/otds.py +0 -0
  20. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm/otiv.py +0 -0
  21. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm/otkd.py +0 -0
  22. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm/otmm.py +0 -0
  23. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm/otpd.py +0 -0
  24. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm/py.typed +0 -0
  25. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_api/__init__.py +0 -0
  26. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_api/__main__.py +0 -0
  27. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_api/app.py +0 -0
  28. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_api/auth/__init__.py +0 -0
  29. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_api/auth/functions.py +0 -0
  30. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_api/auth/models.py +0 -0
  31. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_api/auth/router.py +0 -0
  32. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_api/common/__init__.py +0 -0
  33. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_api/common/functions.py +0 -0
  34. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_api/common/metrics.py +0 -0
  35. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_api/common/models.py +0 -0
  36. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_api/common/router.py +0 -0
  37. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_api/settings.py +0 -0
  38. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_api/terminal/__init__.py +0 -0
  39. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_api/terminal/router.py +0 -0
  40. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_api/v1_maintenance/__init__.py +0 -0
  41. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_api/v1_maintenance/functions.py +0 -0
  42. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_api/v1_maintenance/models.py +0 -0
  43. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_api/v1_maintenance/router.py +0 -0
  44. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_api/v1_otcs/__init__.py +0 -0
  45. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_api/v1_otcs/functions.py +0 -0
  46. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_api/v1_otcs/router.py +0 -0
  47. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_api/v1_payload/__init__.py +0 -0
  48. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_api/v1_payload/functions.py +0 -0
  49. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_api/v1_payload/models.py +0 -0
  50. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_api/v1_payload/router.py +0 -0
  51. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_customizer/__init__.py +0 -0
  52. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_customizer/__main__.py +0 -0
  53. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_customizer/browser_automation.py +0 -0
  54. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_customizer/customizer.py +0 -0
  55. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_customizer/exceptions.py +0 -0
  56. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_customizer/guidewire.py +0 -0
  57. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_customizer/k8s.py +0 -0
  58. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_customizer/log.py +0 -0
  59. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_customizer/m365.py +0 -0
  60. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_customizer/payload_list.py +0 -0
  61. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_customizer/salesforce.py +0 -0
  62. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_customizer/sap.py +0 -0
  63. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_customizer/servicenow.py +0 -0
  64. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_customizer/settings.py +0 -0
  65. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_customizer/successfactors.py +0 -0
  66. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_customizer/translate.py +0 -0
  67. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_maintenance_page/__init__.py +0 -0
  68. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_maintenance_page/__main__.py +0 -0
  69. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_maintenance_page/app.py +0 -0
  70. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_maintenance_page/settings.py +0 -0
  71. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_maintenance_page/static/favicon.avif +0 -0
  72. {pyxecm-3.6.0 → pyxecm-3.6.2}/src/pyxecm_maintenance_page/templates/maintenance.html +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyxecm
3
- Version: 3.6.0
3
+ Version: 3.6.2
4
4
  Summary: A Python library to interact with Opentext Content Management Rest API
5
5
  Project-URL: Homepage, https://github.com/opentext/pyxecm
6
6
  Author-email: Kai Gatzweiler <kgatzweiler@opentext.com>, "Dr. Marc Diefenbruch" <mdiefenb@opentext.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyxecm"
3
- version = "3.6.0"
3
+ version = "3.6.2"
4
4
  description = 'A Python library to interact with Opentext Content Management Rest API'
5
5
  keywords = [
6
6
  'opentext',
@@ -122,18 +122,22 @@ class OTCA:
122
122
  # Stats endpoint:
123
123
  otca_config["usageStatsUrl"] = base_url + "/v1/usage-stats"
124
124
 
125
+ # Tools Url
126
+ tools_url = base_url + "/v1/tools"
127
+ otca_config["toolsUrl"] = tools_url
128
+
125
129
  # MCP Client endpoints:
126
- otca_config["mcpClientServersUrl"] = base_url + "/mcp-client/servers"
127
- otca_config["mcpClientToolsUrl"] = base_url + "/mcp-client/tools"
128
- otca_config["mcpClientToolsRefreshUrl"] = base_url + "/mcp-client/tools/refresh"
129
- otca_config["mcpClientHealthUrl"] = base_url + "/mcp-client/health"
130
+ mcp_client_url = base_url + "/mcp-client"
131
+ otca_config["mcpClientServersUrl"] = mcp_client_url + "/servers"
132
+ otca_config["mcpClientToolsUrl"] = mcp_client_url + "/tools"
133
+ otca_config["mcpClientToolsRefreshUrl"] = otca_config["mcpClientToolsUrl"] + "/refresh"
134
+ otca_config["mcpClientToolsSyncUrl"] = otca_config["mcpClientToolsUrl"] + "/sync"
135
+ otca_config["mcpClientHealthUrl"] = mcp_client_url + "/health"
130
136
 
131
137
  # MCP Server endpoints:
132
- otca_config["mcpServerAllToolsUrl"] = base_url + "/mcp-server/list/alltools"
133
- otca_config["mcpServerRegisterToolsUrl"] = base_url + "/mcp-server/register/tools"
134
- otca_config["mcpServerToolsUrl"] = base_url + "/mcp-server/list/tools"
135
- otca_config["mcpServerDeleteToolsUrl"] = base_url + "/mcp-server/tools"
136
- otca_config["mcpServerHealthUrl"] = base_url + "/mcp-server/health"
138
+ mcp_server_url = base_url + "/mcp-server"
139
+ otca_config["mcpServerToolsUrl"] = mcp_server_url + "/tools"
140
+ otca_config["mcpServerHealthUrl"] = mcp_server_url + "/health"
137
141
 
138
142
  otca_config["contentSystem"] = self.get_content_system()
139
143
  otca_config["clientId"] = client_id
@@ -1268,6 +1272,27 @@ class OTCA:
1268
1272
 
1269
1273
  # end method definition
1270
1274
 
1275
+ def get_tools(self) -> list | None:
1276
+ """List all tools available from remote MCP servers.
1277
+
1278
+ Returns:
1279
+ list | None:
1280
+ List of available MCP tools or None in case of an error.
1281
+
1282
+ """
1283
+
1284
+ request_url = self.config()["toolsUrl"]
1285
+ request_header = self.request_header(service_type="user")
1286
+
1287
+ return self.do_request(
1288
+ url=request_url,
1289
+ method="GET",
1290
+ headers=request_header,
1291
+ failure_message="Canbnot get list of tools",
1292
+ )
1293
+
1294
+ # end method definition
1295
+
1271
1296
  # --- MCP Client Methods ---
1272
1297
 
1273
1298
  def create_mcp_server(self, server_config: dict) -> dict | None:
@@ -1410,6 +1435,29 @@ class OTCA:
1410
1435
  list | None:
1411
1436
  List of available MCP tools or None in case of an error.
1412
1437
 
1438
+ Example:
1439
+ [
1440
+ {
1441
+ 'id': 2,
1442
+ 'key': 'ALLOWED_MCP_TOOLS',
1443
+ 'value': [
1444
+ {
1445
+ 'name': 'current_time',
1446
+ 'description': 'Use this to get the current date and time, return it as a string.'
1447
+ },
1448
+ {
1449
+ 'name': 'otcm_recman_agent',
1450
+ 'description': "Invoke Records Management agent and its related tools."
1451
+ },
1452
+ {
1453
+ 'name': 'otcm_workspace_agent',
1454
+ 'description': "Invoke the workspace operations agent for OpenText Content Management.'
1455
+ }
1456
+ ],
1457
+ 'subscription_id': None
1458
+ }
1459
+ ]
1460
+
1413
1461
  """
1414
1462
 
1415
1463
  request_url = self.config()["mcpClientToolsUrl"]
@@ -1476,7 +1524,7 @@ class OTCA:
1476
1524
 
1477
1525
  """
1478
1526
 
1479
- request_url = self.config()["mcpServerAllToolsUrl"]
1527
+ request_url = self.config()["mcpServerToolsUrl"]
1480
1528
  request_header = self.request_header(service_type="user")
1481
1529
 
1482
1530
  return self.do_request(
@@ -1501,7 +1549,7 @@ class OTCA:
1501
1549
 
1502
1550
  """
1503
1551
 
1504
- request_url = self.config()["mcpServerRegisterToolsUrl"]
1552
+ request_url = self.config()["mcpServerToolsUrl"]
1505
1553
  request_header = self.request_header(service_type="user")
1506
1554
 
1507
1555
  return self.do_request(
@@ -1548,7 +1596,7 @@ class OTCA:
1548
1596
 
1549
1597
  """
1550
1598
 
1551
- request_url = self.config()["mcpServerDeleteToolsUrl"]
1599
+ request_url = self.config()["mcpServerToolsUrl"]
1552
1600
  request_header = self.request_header(service_type="user")
1553
1601
 
1554
1602
  return self.do_request(
@@ -27519,7 +27519,13 @@ class OTCS:
27519
27519
  workspace_id,
27520
27520
  )
27521
27521
  return None
27522
- template_names = [item.text for item in root.findall("startup/processing/templates/template")]
27522
+ template_names = [
27523
+ item.text for item in root.findall("startup/processing/templates/template") if item is not None
27524
+ ]
27525
+ # below for scenarios where they only configure one template for testing
27526
+ if not template_names:
27527
+ # A single template appears only in startup/processing/template instead
27528
+ template_names = [item.text for item in root.findall("startup/processing/template") if item is not None]
27523
27529
 
27524
27530
  return template_names
27525
27531
 
@@ -28189,55 +28195,25 @@ class OTCS:
28189
28195
  if not forms or not isinstance(forms[0], dict):
28190
28196
  continue
28191
28197
 
28198
+ form_data = forms[0].get("data", {})
28199
+ if not isinstance(form_data, dict):
28200
+ form_data = {}
28201
+
28192
28202
  # we take the priority form the form data if
28193
28203
  # the baseline follow-up does not have a priority set.
28194
- form_data = forms[0].get("data", {})
28195
28204
  if "priority" not in followup and "priority" in form_data:
28196
28205
  followup["priority"] = form_data.get("priority")
28197
28206
 
28207
+ # The escalation, activation and schedule details are not part of the reminder
28208
+ # itself - they only live in the view form. We just hang the raw sections the
28209
+ # caller asked for into the followup dictionary. _extract_reminder_payload()
28210
+ # unpacks them into the flat reminder fields further down:
28198
28211
  if escalation:
28199
- escalation_alert = form_data.get("escalation_alert", {}) if isinstance(form_data, dict) else {}
28200
- send_in = escalation_alert.get("send_in", {}) if isinstance(escalation_alert, dict) else {}
28201
- send_to = escalation_alert.get("send_to", []) if isinstance(escalation_alert, dict) else []
28202
-
28203
- if isinstance(escalation_alert, dict) and "escalation_enabled" in escalation_alert:
28204
- followup["escalation_enabled"] = escalation_alert.get("escalation_enabled")
28205
- if isinstance(send_in, dict) and "escalation_period" in send_in:
28206
- followup["escalation_period"] = send_in.get("escalation_period")
28207
- if isinstance(send_in, dict) and "escalation_when" in send_in:
28208
- followup["escalation_when"] = send_in.get("escalation_when")
28209
- if isinstance(send_to, list) and send_to:
28210
- # send_to is a plain list of member IDs; expand each one to the same
28211
- # {id, name, type, type_name, [node_id]} shape as the assignees list:
28212
- escalation_recipients = []
28213
- for recipient_id in send_to:
28214
- recipient_entry = {"id": recipient_id}
28215
- recipient_member = self.get_member(
28216
- member_id=recipient_id, fields="properties{name,type,type_name,node_id}"
28217
- )
28218
- if recipient_member:
28219
- for recipient_field in ("name", "type", "type_name", "node_id"):
28220
- recipient_value = self.get_result_value(
28221
- response=recipient_member, key=recipient_field, show_error=False
28222
- )
28223
- if recipient_value is not None:
28224
- recipient_entry[recipient_field] = recipient_value
28225
- escalation_recipients.append(recipient_entry)
28226
- followup["escalation_recipient_list"] = escalation_recipients
28227
- # end if escalation:
28228
-
28212
+ followup["escalation_alert"] = form_data.get("escalation_alert", {})
28229
28213
  if activation:
28230
- activation_data = form_data.get("activation_alert", {}) if isinstance(form_data, dict) else {}
28231
- send_in = activation_data.get("send_in", {}) if isinstance(activation_data, dict) else {}
28232
- if isinstance(send_in, dict) and "activation_period" in send_in:
28233
- followup["activation_period"] = send_in.get("activation_period")
28234
- if isinstance(send_in, dict) and "activation_unit" in send_in:
28235
- followup["activation_unit"] = send_in.get("activation_unit")
28236
-
28237
- if schedule:
28238
- schedule_data = form_data.get("schedule", {}) if isinstance(form_data, dict) else {}
28239
- if schedule_data:
28240
- followup["schedule"] = schedule_data
28214
+ followup["activation_alert"] = form_data.get("activation_alert", {})
28215
+ if schedule and form_data.get("schedule"):
28216
+ followup["schedule"] = form_data["schedule"]
28241
28217
  # end for loop to enrich reminders with view form information
28242
28218
 
28243
28219
  # Normalize the numeric values of the REST API response to their readable form.
@@ -28466,58 +28442,24 @@ class OTCS:
28466
28442
  return self._extract_reminder_payload(reminder=response)
28467
28443
 
28468
28444
  form_data = forms[0].get("data", {})
28445
+ if not isinstance(form_data, dict):
28446
+ form_data = {}
28469
28447
 
28470
28448
  # we take the priority from the form data if
28471
28449
  # the baseline follow-up does not have a priority set.
28472
28450
  if "priority" not in followup and "priority" in form_data:
28473
28451
  followup["priority"] = form_data.get("priority")
28474
28452
 
28453
+ # The escalation, activation and schedule details are not part of the reminder itself -
28454
+ # they only live in the view form. We just hang the raw sections the caller asked for
28455
+ # into the followup dictionary. _extract_reminder_payload() below unpacks them into the
28456
+ # flat reminder fields and normalizes their values:
28475
28457
  if escalation:
28476
- escalation_alert = form_data.get("escalation_alert", {}) if isinstance(form_data, dict) else {}
28477
- send_in = escalation_alert.get("send_in", {}) if isinstance(escalation_alert, dict) else {}
28478
- send_to = escalation_alert.get("send_to", {}) if isinstance(escalation_alert, dict) else []
28479
-
28480
- # The fields 'escalation_enabled', 'escalation_period', and 'escalation_when' may only be aviable
28481
- # if the reminder is in 'open' state. If it is already in 'active' or 'completed' state, these
28482
- # fields may not be available.
28483
- if isinstance(escalation_alert, dict) and "escalation_enabled" in escalation_alert:
28484
- followup["escalation_enabled"] = escalation_alert.get("escalation_enabled")
28485
- if isinstance(send_in, dict) and "escalation_period" in send_in:
28486
- followup["escalation_period"] = send_in.get("escalation_period")
28487
- if isinstance(send_in, dict) and "escalation_when" in send_in:
28488
- followup["escalation_when"] = send_in.get("escalation_when")
28489
-
28490
- if isinstance(send_to, list) and send_to:
28491
- # send_to is a plain list of member IDs; expand each one to the same
28492
- # {id, name, type, type_name, [node_id]} shape as the assignees list:
28493
- escalation_recipients = []
28494
- for escalation_recipient_id in send_to:
28495
- escalation_recipient_entry = {"id": escalation_recipient_id}
28496
- escalation_recipient_member = self.get_member(
28497
- member_id=escalation_recipient_id, fields="properties{name_formatted,type,type_name,node_id}"
28498
- )
28499
- if escalation_recipient_member:
28500
- for escalation_recipient_field in ("name_formatted", "type", "type_name", "node_id"):
28501
- escalation_recipient_value = self.get_result_value(
28502
- response=escalation_recipient_member, key=escalation_recipient_field, show_error=False
28503
- )
28504
- if escalation_recipient_value is not None:
28505
- if escalation_recipient_field == "name_formatted":
28506
- escalation_recipient_field = "name"
28507
- escalation_recipient_entry[escalation_recipient_field] = escalation_recipient_value
28508
- escalation_recipients.append(escalation_recipient_entry)
28509
- followup["escalation_recipient_list"] = escalation_recipients
28458
+ followup["escalation_alert"] = form_data.get("escalation_alert", {})
28510
28459
  if activation:
28511
- activation_data = form_data.get("activation_alert", {}) if isinstance(form_data, dict) else {}
28512
- send_in = activation_data.get("send_in", {}) if isinstance(activation_data, dict) else {}
28513
- if isinstance(send_in, dict) and "activation_period" in send_in:
28514
- followup["activation_period"] = send_in.get("activation_period")
28515
- if isinstance(send_in, dict) and "activation_unit" in send_in:
28516
- followup["activation_unit"] = send_in.get("activation_unit")
28517
- if schedule:
28518
- schedule_data = form_data.get("schedule", {}) if isinstance(form_data, dict) else {}
28519
- if schedule_data:
28520
- followup["schedule"] = schedule_data
28460
+ followup["activation_alert"] = form_data.get("activation_alert", {})
28461
+ if schedule and form_data.get("schedule"):
28462
+ followup["schedule"] = form_data["schedule"]
28521
28463
 
28522
28464
  # Normalize the numeric values of the REST API response to their readable form.
28523
28465
  # This happens last, so that the values added from the view form above are covered as well:
@@ -29093,6 +29035,16 @@ class OTCS:
29093
29035
  escalation_when -1 -> "after"
29094
29036
  activation_unit 1 -> "day"
29095
29037
 
29038
+ In addition, this method unpacks the raw sections of the reminder view form that
29039
+ ``get_reminder()`` and ``get_reminders()`` hang into the reminder: "escalation_alert"
29040
+ becomes ``escalation_enabled``, ``escalation_period``, ``escalation_when``,
29041
+ ``escalation_unit`` and ``escalation_recipient_list`` (each recipient expanded via
29042
+ ``get_member()``), and "activation_alert" becomes ``activation_period``,
29043
+ ``activation_unit`` and ``activation_when``. The raw sections are removed afterwards.
29044
+ Depending on the state of the reminder, OTCS delivers these settings either as
29045
+ discrete fields (state "open") or as readable text in "activationAlert1" /
29046
+ "activationAlert2" (states "active" and "in progress") - both forms are handled.
29047
+
29096
29048
  NOTE: Values that are only delivered by the reminder view form (priority, escalation,
29097
29049
  activation and the relative due date) are only present if ``get_reminder()`` or
29098
29050
  ``get_reminders()`` were called with ``escalation=True``, ``activation=True`` or
@@ -29100,6 +29052,9 @@ class OTCS:
29100
29052
  encoding is kept as it is and reported with a warning - this method never
29101
29053
  guesses and never removes information.
29102
29054
 
29055
+ NOTE: Expanding the escalation recipients issues one ``get_member()`` request per
29056
+ recipient.
29057
+
29103
29058
  Args:
29104
29059
  reminder (dict | None):
29105
29060
  A reminder as returned by ``get_reminder()``, a single entry as yielded by
@@ -29148,10 +29103,99 @@ class OTCS:
29148
29103
  return
29149
29104
  data[field] = names[value]
29150
29105
 
29106
+ # get_reminder() and get_reminders() hang the raw "escalation_alert" and
29107
+ # "activation_alert" sections of the reminder view form into the followup dictionary -
29108
+ # but only if the caller asked for them. They are unpacked into the flat reminder
29109
+ # fields here and dropped afterwards, so that callers only ever see the flat fields.
29110
+ # The numeric values they carry are normalized by the map_value() calls below:
29111
+ escalation_alert = followup.pop("escalation_alert", None)
29112
+ if isinstance(escalation_alert, dict):
29113
+ send_in = escalation_alert.get("send_in", {})
29114
+ if not isinstance(send_in, dict):
29115
+ send_in = {}
29116
+ send_to = escalation_alert.get("send_to", [])
29117
+
29118
+ # The fields 'escalation_enabled', 'escalation_period', and 'escalation_when' may only
29119
+ # be available if the reminder is in 'open' state. If it is already in 'active' or
29120
+ # 'in progress' state, OTCS reports them as readable text in 'activationAlert1' and
29121
+ # 'activationAlert2' instead:
29122
+ if "escalation_enabled" in escalation_alert:
29123
+ followup["escalation_enabled"] = escalation_alert.get("escalation_enabled")
29124
+ else:
29125
+ followup["escalation_enabled"] = "activationAlert1" in send_in
29126
+
29127
+ if "escalation_period" in send_in:
29128
+ followup["escalation_period"] = send_in.get("escalation_period")
29129
+ elif "activationAlert1" in send_in:
29130
+ followup["escalation_period"] = int(send_in.get("activationAlert1", "0").split(" ", 1)[0])
29131
+
29132
+ if "escalation_when" in send_in:
29133
+ followup["escalation_when"] = send_in.get("escalation_when")
29134
+ elif "activationAlert2" in send_in:
29135
+ followup["escalation_when"] = self.REMINDER_ESCALATION_WHEN_MAP.get(
29136
+ "after" if "after" in send_in.get("activationAlert2", "") else "before",
29137
+ )
29138
+
29139
+ for unit in self.REMINDER_UNIT_MAP:
29140
+ if unit in send_in.get("activationAlert1", ""):
29141
+ followup["escalation_unit"] = self.REMINDER_UNIT_MAP[unit]
29142
+ break
29143
+
29144
+ if isinstance(send_to, list) and send_to:
29145
+ # send_to is a plain list of member IDs; expand each one to the same
29146
+ # {id, name, type, type_name, [node_id]} shape as the assignees list:
29147
+ escalation_recipients = []
29148
+ for recipient_id in send_to:
29149
+ recipient_entry = {"id": recipient_id}
29150
+ recipient_member = self.get_member(
29151
+ member_id=recipient_id,
29152
+ fields="properties{name_formatted,type,type_name,node_id}",
29153
+ )
29154
+ if recipient_member:
29155
+ for recipient_field in ("name_formatted", "type", "type_name", "node_id"):
29156
+ recipient_value = self.get_result_value(
29157
+ response=recipient_member, key=recipient_field, show_error=False
29158
+ )
29159
+ if recipient_value is not None:
29160
+ # the members REST API calls the name "name_formatted":
29161
+ key = "name" if recipient_field == "name_formatted" else recipient_field
29162
+ recipient_entry[key] = recipient_value
29163
+ escalation_recipients.append(recipient_entry)
29164
+ followup["escalation_recipient_list"] = escalation_recipients
29165
+ # end if isinstance(send_to, list) and send_to
29166
+ # end if isinstance(escalation_alert, dict)
29167
+
29168
+ activation_alert = followup.pop("activation_alert", None)
29169
+ if isinstance(activation_alert, dict):
29170
+ send_in = activation_alert.get("send_in", {})
29171
+ if not isinstance(send_in, dict):
29172
+ send_in = {}
29173
+
29174
+ if "activation_period" in send_in:
29175
+ followup["activation_period"] = send_in.get("activation_period")
29176
+ elif "activationAlert1" in send_in:
29177
+ followup["activation_period"] = int(send_in.get("activationAlert1", "0").split(" ", 1)[0])
29178
+
29179
+ if "activation_unit" in send_in:
29180
+ followup["activation_unit"] = send_in.get("activation_unit")
29181
+ else:
29182
+ for unit in self.REMINDER_UNIT_MAP:
29183
+ if unit in send_in.get("activationAlert1", ""):
29184
+ followup["activation_unit"] = self.REMINDER_UNIT_MAP[unit]
29185
+ break
29186
+
29187
+ if "activationAlert2" in send_in:
29188
+ followup["activation_when"] = self.REMINDER_ESCALATION_WHEN_MAP.get(
29189
+ "after" if "after" in send_in.get("activationAlert2", "") else "before",
29190
+ )
29191
+ # end if isinstance(activation_alert, dict)
29192
+
29151
29193
  map_value(followup, "status", self.REMINDER_STATUS_NAMES)
29152
29194
  map_value(followup, "priority", self.REMINDER_PRIORITY_NAMES)
29153
29195
  map_value(followup, "rule", self.REMINDER_RULE_NAMES)
29154
29196
  map_value(followup, "escalation_when", self.REMINDER_ESCALATION_WHEN_NAMES)
29197
+ map_value(followup, "escalation_unit", self.REMINDER_UNIT_NAMES)
29198
+ map_value(followup, "activation_when", self.REMINDER_ESCALATION_WHEN_NAMES)
29155
29199
  map_value(followup, "activation_unit", self.REMINDER_UNIT_NAMES)
29156
29200
 
29157
29201
  # The unit of a relative due date lives in the schedule delivered by the view form:
@@ -29160,6 +29204,20 @@ class OTCS:
29160
29204
  due_in = schedule.get("due_in")
29161
29205
  if isinstance(due_in, dict):
29162
29206
  map_value(due_in, "due_in_unit", self.REMINDER_UNIT_NAMES)
29207
+ seq_repeat = schedule.get("seq_repeat")
29208
+ if seq_repeat:
29209
+ if "Every day" in seq_repeat:
29210
+ schedule["repeat_on"] = OTCS.REMINDER_SEQUENCE_MAP.get("daily")
29211
+ if "Every 2 weeks" in seq_repeat:
29212
+ schedule["repeat_on"] = OTCS.REMINDER_SEQUENCE_MAP.get("bi-weekly")
29213
+ elif "Every 2 months" in seq_repeat:
29214
+ schedule["repeat_on"] = OTCS.REMINDER_SEQUENCE_MAP.get("every 2 months")
29215
+ elif "every 3 months" in seq_repeat:
29216
+ schedule["repeat_on"] = OTCS.REMINDER_SEQUENCE_MAP.get("quarterly")
29217
+ elif "every 6 months" in seq_repeat:
29218
+ schedule["repeat_on"] = OTCS.REMINDER_SEQUENCE_MAP.get("semi-annually")
29219
+ elif "Every year" in seq_repeat:
29220
+ schedule["repeat_on"] = OTCS.REMINDER_SEQUENCE_MAP.get("yearly")
29163
29221
 
29164
29222
  # OTCS reports this flag as 0 / 1 while the reminder methods take a boolean:
29165
29223
  if isinstance(followup.get("activation_by_day"), int):
@@ -608,6 +608,7 @@ class Payload:
608
608
  self._embeddings = self.get_payload_section("embeddings")
609
609
  self._nifi_flows = self.get_payload_section("nifi")
610
610
  self._aviator_mcp_servers = self.get_payload_section("aviatorMcpServers")
611
+ self._aviator_mcp_tools = self.get_payload_section("aviatorMcpTools")
611
612
 
612
613
  return self._payload
613
614
 
@@ -2951,6 +2952,14 @@ class Payload:
2951
2952
  self.logger.warning(
2952
2953
  "aviatorMcpServers in payload but Content Aviator (OTCA) is not configured! MCP servers will not be processed.",
2953
2954
  )
2955
+ case "aviatorMcpTools":
2956
+ if self._otca and isinstance(self._otca, OTCA):
2957
+ self._log_header_callback("Process Aviator MCP Tools")
2958
+ self.process_mcp_tools()
2959
+ else:
2960
+ self.logger.warning(
2961
+ "aviatorMcpTools in payload but Content Aviator (OTCA) is not configured! MCP tools will not be processed.",
2962
+ )
2954
2963
  case _:
2955
2964
  self.logger.error(
2956
2965
  "Illegal payload section name -> '%s' in payloadSections!",
@@ -32224,3 +32233,137 @@ class Payload:
32224
32233
  return success
32225
32234
 
32226
32235
  # end method definition
32236
+
32237
+ @tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="process_mcp_tools")
32238
+ def process_mcp_tools(self, section_name: str = "aviatorMcpTools") -> bool:
32239
+ """Register Content Aviator agents as MCP tools.
32240
+
32241
+ Payload item keys:
32242
+ - enabled (bool, optional, default=True)
32243
+ - name (str, required)
32244
+
32245
+ Args:
32246
+ section_name (str, optional):
32247
+ The name of the payload section. It can be overridden
32248
+ for cases where multiple sections of same type
32249
+ are used (e.g. the "Post" sections).
32250
+ This name is also used for the "success" status
32251
+ files written to the Admin Personal Workspace.
32252
+
32253
+ Returns:
32254
+ bool:
32255
+ True, if payload has been processed without errors, False otherwise.
32256
+
32257
+ """
32258
+
32259
+ if not self._aviator_mcp_tools:
32260
+ self.logger.info(
32261
+ "Payload section -> '%s' is empty. Skipping...",
32262
+ section_name,
32263
+ )
32264
+ return True
32265
+
32266
+ # If this payload section has been processed successfully before we
32267
+ # can return True and skip processing it once more:
32268
+ if self.check_status_file(payload_section_name=section_name):
32269
+ return True
32270
+
32271
+ success: bool = True
32272
+
32273
+ # list_mcp_tools() returns a list of settings entries (e.g. key -> 'ALLOWED_MCP_TOOLS')
32274
+ # whose "value" is the actual list of tool dictionaries with "name" and "description":
32275
+ existing_mcp_tools = self._otca.list_registered_mcp_tools()
32276
+ if existing_mcp_tools is None:
32277
+ self.logger.warning(
32278
+ "Failed to retrieve the list of available MCP tools from Content Aviator. Cannot check if tools are already registered."
32279
+ )
32280
+
32281
+ existing_mcp_tool_names = [
32282
+ tool.get("name")
32283
+ for entry in existing_mcp_tools or []
32284
+ for tool in entry.get("value") or []
32285
+ if tool.get("name")
32286
+ ]
32287
+ if existing_mcp_tool_names:
32288
+ self.logger.info(
32289
+ "Already registered MCP tools in Content Aviator: %s",
32290
+ ", ".join(existing_mcp_tool_names),
32291
+ )
32292
+
32293
+ for mcp_tool in self._aviator_mcp_tools:
32294
+ tool_name = mcp_tool.get("name")
32295
+ if not tool_name:
32296
+ self.logger.error("Content Aviator MCP tool needs a name in payload! Skipping...")
32297
+ success = False
32298
+ continue
32299
+
32300
+ # Check if element has been disabled in payload (enabled = false).
32301
+ # In this case we skip the element:
32302
+ if not mcp_tool.get("enabled", True):
32303
+ self.logger.info(
32304
+ "Payload for Content Aviator MCP tool -> '%s' is disabled. Skipping...",
32305
+ tool_name,
32306
+ )
32307
+ continue
32308
+
32309
+ if tool_name in existing_mcp_tool_names:
32310
+ self.logger.info(
32311
+ "Content Aviator MCP tool -> '%s' is already registered in Content Aviator. No need to register it again. Skipping...",
32312
+ tool_name,
32313
+ )
32314
+ continue
32315
+
32316
+ response = self._otca.register_mcp_tools(tools=[tool_name])
32317
+ if response:
32318
+ tool_id = response.get("id", "Unknown ID")
32319
+ # The response value is the complete list of tools registered for the
32320
+ # tenant - we need to pick the entry of the tool we just registered:
32321
+ value = response.get("value") or []
32322
+ tool_entry = next((entry for entry in value if entry.get("name") == tool_name), None)
32323
+ if tool_entry is None:
32324
+ self.logger.warning(
32325
+ "Content Aviator MCP tool -> '%s' is not included in the registration response!",
32326
+ tool_name,
32327
+ )
32328
+ tool_description = (
32329
+ tool_entry.get("description", "Unknown Description") if tool_entry else "Unknown Description"
32330
+ )
32331
+ self.logger.info(
32332
+ "Registered Content Aviator MCP tool -> '%s' (%s).",
32333
+ tool_name,
32334
+ tool_id,
32335
+ )
32336
+ mcp_tool["id"] = tool_id # record the ID of the MCP tool settings record in the payload
32337
+ mcp_tool["description"] = tool_description # record the created tool description in the payload
32338
+ mcp_tool["registered"] = True
32339
+ else:
32340
+ self.logger.error(
32341
+ "Failed to register Content Aviator MCP tool -> '%s'!",
32342
+ tool_name,
32343
+ )
32344
+ mcp_tool["registered"] = False
32345
+ success = False
32346
+ # end for mcp_tool in self._aviator_mcp_tools
32347
+
32348
+ registered_tools = [tool.get("name") for tool in self._aviator_mcp_tools if tool.get("registered") is True]
32349
+ if registered_tools:
32350
+ self.logger.info(
32351
+ "Successfully registered the following additional MCP tools: %s",
32352
+ registered_tools,
32353
+ )
32354
+ failed_tools = [tool.get("name") for tool in self._aviator_mcp_tools if tool.get("registered") is False]
32355
+ if failed_tools:
32356
+ self.logger.info(
32357
+ "Failed to register the following MCP tools: %s",
32358
+ failed_tools,
32359
+ )
32360
+
32361
+ self.write_status_file(
32362
+ success=success,
32363
+ payload_section_name=section_name,
32364
+ payload_section=self._aviator_mcp_tools,
32365
+ )
32366
+
32367
+ return success
32368
+
32369
+ # end method definition
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes