pyxecm 3.0.1__py3-none-any.whl → 3.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pyxecm might be problematic. Click here for more details.
- pyxecm/avts.py +4 -4
- pyxecm/coreshare.py +14 -15
- pyxecm/helper/data.py +2 -1
- pyxecm/helper/web.py +11 -11
- pyxecm/helper/xml.py +41 -10
- pyxecm/otac.py +1 -1
- pyxecm/otawp.py +19 -19
- pyxecm/otca.py +878 -70
- pyxecm/otcs.py +1716 -349
- pyxecm/otds.py +332 -153
- pyxecm/otkd.py +4 -4
- pyxecm/otmm.py +1 -1
- pyxecm/otpd.py +246 -30
- {pyxecm-3.0.1.dist-info → pyxecm-3.1.1.dist-info}/METADATA +2 -1
- pyxecm-3.1.1.dist-info/RECORD +82 -0
- pyxecm_api/app.py +45 -35
- pyxecm_api/auth/functions.py +2 -2
- pyxecm_api/auth/router.py +2 -3
- pyxecm_api/common/functions.py +67 -12
- pyxecm_api/settings.py +0 -8
- pyxecm_api/terminal/router.py +1 -1
- pyxecm_api/v1_csai/router.py +33 -18
- pyxecm_customizer/browser_automation.py +161 -79
- pyxecm_customizer/customizer.py +43 -25
- pyxecm_customizer/guidewire.py +422 -8
- pyxecm_customizer/k8s.py +23 -27
- pyxecm_customizer/knowledge_graph.py +498 -20
- pyxecm_customizer/m365.py +45 -44
- pyxecm_customizer/payload.py +1723 -1188
- pyxecm_customizer/payload_list.py +3 -0
- pyxecm_customizer/salesforce.py +122 -79
- pyxecm_customizer/servicenow.py +27 -7
- pyxecm_customizer/settings.py +3 -1
- pyxecm_customizer/successfactors.py +2 -2
- pyxecm_customizer/translate.py +1 -1
- pyxecm-3.0.1.dist-info/RECORD +0 -96
- pyxecm_api/agents/__init__.py +0 -7
- pyxecm_api/agents/app.py +0 -13
- pyxecm_api/agents/functions.py +0 -119
- pyxecm_api/agents/models.py +0 -10
- pyxecm_api/agents/otcm_knowledgegraph/__init__.py +0 -1
- pyxecm_api/agents/otcm_knowledgegraph/functions.py +0 -85
- pyxecm_api/agents/otcm_knowledgegraph/models.py +0 -61
- pyxecm_api/agents/otcm_knowledgegraph/router.py +0 -74
- pyxecm_api/agents/otcm_user_agent/__init__.py +0 -1
- pyxecm_api/agents/otcm_user_agent/models.py +0 -20
- pyxecm_api/agents/otcm_user_agent/router.py +0 -65
- pyxecm_api/agents/otcm_workspace_agent/__init__.py +0 -1
- pyxecm_api/agents/otcm_workspace_agent/models.py +0 -40
- pyxecm_api/agents/otcm_workspace_agent/router.py +0 -200
- {pyxecm-3.0.1.dist-info → pyxecm-3.1.1.dist-info}/WHEEL +0 -0
- {pyxecm-3.0.1.dist-info → pyxecm-3.1.1.dist-info}/entry_points.txt +0 -0
pyxecm/otcs.py
CHANGED
|
@@ -14,6 +14,8 @@ __maintainer__ = "Dr. Marc Diefenbruch"
|
|
|
14
14
|
__email__ = "mdiefenb@opentext.com"
|
|
15
15
|
|
|
16
16
|
import asyncio
|
|
17
|
+
import hashlib
|
|
18
|
+
import html
|
|
17
19
|
import json
|
|
18
20
|
import logging
|
|
19
21
|
import mimetypes
|
|
@@ -26,6 +28,7 @@ import tempfile
|
|
|
26
28
|
import threading
|
|
27
29
|
import time
|
|
28
30
|
import urllib.parse
|
|
31
|
+
import xml.etree.ElementTree as ET
|
|
29
32
|
import zipfile
|
|
30
33
|
from concurrent.futures import ThreadPoolExecutor
|
|
31
34
|
from datetime import UTC, datetime
|
|
@@ -73,8 +76,8 @@ REQUEST_DOWNLOAD_HEADERS = {
|
|
|
73
76
|
"Content-Type": "application/json",
|
|
74
77
|
}
|
|
75
78
|
|
|
76
|
-
REQUEST_TIMEOUT = 60
|
|
77
|
-
REQUEST_RETRY_DELAY = 30
|
|
79
|
+
REQUEST_TIMEOUT = 60.0
|
|
80
|
+
REQUEST_RETRY_DELAY = 30.0
|
|
78
81
|
REQUEST_MAX_RETRIES = 4
|
|
79
82
|
|
|
80
83
|
default_logger = logging.getLogger(MODULE_NAME)
|
|
@@ -116,6 +119,7 @@ class OTCS:
|
|
|
116
119
|
|
|
117
120
|
ITEM_TYPE_BUSINESS_WORKSPACE = 848
|
|
118
121
|
ITEM_TYPE_CATEGORY = 131
|
|
122
|
+
ITEM_TYPE_CATEGORY_FOLDER = 132
|
|
119
123
|
ITEM_TYPE_CHANNEL = 207
|
|
120
124
|
ITEM_TYPE_CLASSIFICATION_TREE = 196
|
|
121
125
|
ITEM_TYPE_CLASSIFICATION = 199
|
|
@@ -157,9 +161,11 @@ class OTCS:
|
|
|
157
161
|
ITEM_TYPE_BUSINESS_WORKSPACE,
|
|
158
162
|
ITEM_TYPE_COMPOUND_DOCUMENT,
|
|
159
163
|
ITEM_TYPE_CLASSIFICATION,
|
|
164
|
+
ITEM_TYPE_CATEGORY_FOLDER,
|
|
160
165
|
VOLUME_TYPE_ENTERPRISE_WORKSPACE,
|
|
161
166
|
VOLUME_TYPE_CLASSIFICATION_VOLUME,
|
|
162
167
|
VOLUME_TYPE_CONTENT_SERVER_DOCUMENT_TEMPLATES,
|
|
168
|
+
VOLUME_TYPE_CATEGORIES_VOLUME,
|
|
163
169
|
]
|
|
164
170
|
|
|
165
171
|
PERMISSION_TYPES = [
|
|
@@ -187,6 +193,7 @@ class OTCS:
|
|
|
187
193
|
_config: dict
|
|
188
194
|
_otcs_ticket = None
|
|
189
195
|
_otds_ticket = None
|
|
196
|
+
_otds_token = None
|
|
190
197
|
_data: Data = None
|
|
191
198
|
_thread_number = 3
|
|
192
199
|
_download_dir = ""
|
|
@@ -323,6 +330,7 @@ class OTCS:
|
|
|
323
330
|
resource_id: str = "",
|
|
324
331
|
default_license: str = "X3",
|
|
325
332
|
otds_ticket: str | None = None,
|
|
333
|
+
otds_token: str | None = None,
|
|
326
334
|
base_path: str = "/cs/cs",
|
|
327
335
|
support_path: str = "/cssupport",
|
|
328
336
|
thread_number: int = 3,
|
|
@@ -357,6 +365,8 @@ class OTCS:
|
|
|
357
365
|
The name of the default user license. Default is "X3".
|
|
358
366
|
otds_ticket (str, optional):
|
|
359
367
|
The authentication ticket of OTDS.
|
|
368
|
+
otds_token (str, optional):
|
|
369
|
+
The authentication token of OTDS.
|
|
360
370
|
base_path (str, optional):
|
|
361
371
|
The base path segment of the Content Server URL.
|
|
362
372
|
This typically is /cs/cs on a Linux deployment or /cs/cs.exe
|
|
@@ -392,52 +402,16 @@ class OTCS:
|
|
|
392
402
|
# Initialize otcs_config as an empty dictionary
|
|
393
403
|
otcs_config = {}
|
|
394
404
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
otcs_config["hostname"] = "otcs-admin-0"
|
|
399
|
-
|
|
400
|
-
if protocol:
|
|
401
|
-
otcs_config["protocol"] = protocol
|
|
402
|
-
else:
|
|
403
|
-
otcs_config["protocol"] = "http"
|
|
404
|
-
|
|
405
|
-
if port:
|
|
406
|
-
otcs_config["port"] = port
|
|
407
|
-
else:
|
|
408
|
-
otcs_config["port"] = 8080
|
|
409
|
-
|
|
405
|
+
otcs_config["hostname"] = hostname or "otcs-admin-0"
|
|
406
|
+
otcs_config["protocol"] = protocol or "http"
|
|
407
|
+
otcs_config["port"] = port or 8080
|
|
410
408
|
otcs_config["publicUrl"] = public_url
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
if password:
|
|
418
|
-
otcs_config["password"] = password
|
|
419
|
-
else:
|
|
420
|
-
otcs_config["password"] = ""
|
|
421
|
-
|
|
422
|
-
if user_partition:
|
|
423
|
-
otcs_config["partition"] = user_partition
|
|
424
|
-
else:
|
|
425
|
-
otcs_config["partition"] = ""
|
|
426
|
-
|
|
427
|
-
if resource_name:
|
|
428
|
-
otcs_config["resource"] = resource_name
|
|
429
|
-
else:
|
|
430
|
-
otcs_config["resource"] = ""
|
|
431
|
-
|
|
432
|
-
if resource_id:
|
|
433
|
-
otcs_config["resourceId"] = resource_id
|
|
434
|
-
else:
|
|
435
|
-
otcs_config["resourceId"] = None
|
|
436
|
-
|
|
437
|
-
if default_license:
|
|
438
|
-
otcs_config["license"] = default_license
|
|
439
|
-
else:
|
|
440
|
-
otcs_config["license"] = ""
|
|
409
|
+
otcs_config["username"] = username or "admin"
|
|
410
|
+
otcs_config["password"] = password or ""
|
|
411
|
+
otcs_config["partition"] = user_partition or ""
|
|
412
|
+
otcs_config["resource"] = resource_name or ""
|
|
413
|
+
otcs_config["resourceId"] = resource_id or None
|
|
414
|
+
otcs_config["license"] = default_license or ""
|
|
441
415
|
|
|
442
416
|
otcs_config["femeUri"] = feme_uri
|
|
443
417
|
|
|
@@ -500,7 +474,10 @@ class OTCS:
|
|
|
500
474
|
otcs_config["holdsUrl"] = otcs_rest_url + "/v1/holds"
|
|
501
475
|
otcs_config["holdsUrlv2"] = otcs_rest_url + "/v2/holds"
|
|
502
476
|
otcs_config["validationUrl"] = otcs_rest_url + "/v1/validation/nodes/names"
|
|
503
|
-
otcs_config["aiUrl"] = otcs_rest_url + "/v2/ai
|
|
477
|
+
otcs_config["aiUrl"] = otcs_rest_url + "/v2/ai"
|
|
478
|
+
otcs_config["aiNodesUrl"] = otcs_config["aiUrl"] + "/nodes"
|
|
479
|
+
otcs_config["aiChatUrl"] = otcs_config["aiUrl"] + "/chat"
|
|
480
|
+
otcs_config["aiContextUrl"] = otcs_config["aiUrl"] + "/context"
|
|
504
481
|
otcs_config["recycleBinUrl"] = otcs_rest_url + "/v2/volumes/recyclebin"
|
|
505
482
|
otcs_config["processUrl"] = otcs_rest_url + "/v2/processes"
|
|
506
483
|
otcs_config["workflowUrl"] = otcs_rest_url + "/v2/workflows"
|
|
@@ -510,9 +487,11 @@ class OTCS:
|
|
|
510
487
|
otcs_config["nodesFormUrl"] = otcs_rest_url + "/v1/forms/nodes"
|
|
511
488
|
otcs_config["draftProcessFormUrl"] = otcs_rest_url + "/v1/forms/draftprocesses"
|
|
512
489
|
otcs_config["processTaskUrl"] = otcs_rest_url + "/v1/forms/processes/tasks/update"
|
|
490
|
+
otcs_config["docGenUrl"] = otcs_url + "?func=xecmpfdocgen"
|
|
513
491
|
|
|
514
492
|
self._config = otcs_config
|
|
515
493
|
self._otds_ticket = otds_ticket
|
|
494
|
+
self._otds_token = otds_token
|
|
516
495
|
self._data = Data(logger=self.logger)
|
|
517
496
|
self._thread_number = thread_number
|
|
518
497
|
self._download_dir = download_dir
|
|
@@ -521,6 +500,7 @@ class OTCS:
|
|
|
521
500
|
self._use_numeric_category_identifier = use_numeric_category_identifier
|
|
522
501
|
self._executor = ThreadPoolExecutor(max_workers=thread_number)
|
|
523
502
|
self._workspace_type_lookup = {}
|
|
503
|
+
self._workspace_type_names = []
|
|
524
504
|
|
|
525
505
|
# end method definition
|
|
526
506
|
|
|
@@ -567,6 +547,34 @@ class OTCS:
|
|
|
567
547
|
|
|
568
548
|
# end method definition
|
|
569
549
|
|
|
550
|
+
def otcs_ticket_hashed(self) -> str | None:
|
|
551
|
+
"""Return the hashed OTCS ticket.
|
|
552
|
+
|
|
553
|
+
Returns:
|
|
554
|
+
str | None:
|
|
555
|
+
The hashed OTCS ticket (which may be None).
|
|
556
|
+
|
|
557
|
+
"""
|
|
558
|
+
|
|
559
|
+
if not self._otcs_ticket:
|
|
560
|
+
return None
|
|
561
|
+
|
|
562
|
+
# Encode the input string before hashing
|
|
563
|
+
encoded_string = self._otcs_ticket.encode("utf-8")
|
|
564
|
+
|
|
565
|
+
# Create a new SHA-512 hash object
|
|
566
|
+
sha512 = hashlib.sha512()
|
|
567
|
+
|
|
568
|
+
# Update the hash object with the input string
|
|
569
|
+
sha512.update(encoded_string)
|
|
570
|
+
|
|
571
|
+
# Get the hexadecimal representation of the hash
|
|
572
|
+
hashed_output = sha512.hexdigest()
|
|
573
|
+
|
|
574
|
+
return hashed_output
|
|
575
|
+
|
|
576
|
+
# end method definition
|
|
577
|
+
|
|
570
578
|
def set_otcs_ticket(self, ticket: str) -> None:
|
|
571
579
|
"""Set the OTCS ticket.
|
|
572
580
|
|
|
@@ -593,6 +601,19 @@ class OTCS:
|
|
|
593
601
|
|
|
594
602
|
# end method definition
|
|
595
603
|
|
|
604
|
+
def set_otds_token(self, token: str) -> None:
|
|
605
|
+
"""Set the OTDS token.
|
|
606
|
+
|
|
607
|
+
Args:
|
|
608
|
+
token (str):
|
|
609
|
+
The new OTDS token.
|
|
610
|
+
|
|
611
|
+
"""
|
|
612
|
+
|
|
613
|
+
self._otds_token = token
|
|
614
|
+
|
|
615
|
+
# end method definition
|
|
616
|
+
|
|
596
617
|
def credentials(self) -> dict:
|
|
597
618
|
"""Get credentials (username + password).
|
|
598
619
|
|
|
@@ -903,7 +924,7 @@ class OTCS:
|
|
|
903
924
|
data: dict | None = None,
|
|
904
925
|
json_data: dict | None = None,
|
|
905
926
|
files: dict | None = None,
|
|
906
|
-
timeout:
|
|
927
|
+
timeout: float | None = REQUEST_TIMEOUT,
|
|
907
928
|
show_error: bool = True,
|
|
908
929
|
show_warning: bool = False,
|
|
909
930
|
warning_message: str = "",
|
|
@@ -931,7 +952,7 @@ class OTCS:
|
|
|
931
952
|
Dictionary of {"name": file-tuple} for multipart encoding upload.
|
|
932
953
|
The file-tuple can be a 2-tuple ("filename", fileobj) or a 3-tuple
|
|
933
954
|
("filename", fileobj, "content_type").
|
|
934
|
-
timeout (
|
|
955
|
+
timeout (float | None, optional):
|
|
935
956
|
Timeout for the request in seconds. Defaults to REQUEST_TIMEOUT.
|
|
936
957
|
show_error (bool, optional):
|
|
937
958
|
Whether or not an error should be logged in case of a failed REST call.
|
|
@@ -998,7 +1019,12 @@ class OTCS:
|
|
|
998
1019
|
if success_message:
|
|
999
1020
|
self.logger.info(success_message)
|
|
1000
1021
|
if parse_request_response and not stream:
|
|
1001
|
-
|
|
1022
|
+
# There are cases where OTCS returns response.ok (200) but
|
|
1023
|
+
# because of restart or scaling of pods the response text is not
|
|
1024
|
+
# valid JSON. So parse_request_response() may raise an ConnectionError exception that
|
|
1025
|
+
# is handled in the exception block below (with waiting for readiness and retry logic)
|
|
1026
|
+
parsed_response = self.parse_request_response(response_object=response)
|
|
1027
|
+
return parsed_response
|
|
1002
1028
|
else:
|
|
1003
1029
|
return response
|
|
1004
1030
|
# Check if Session has expired - then re-authenticate and try once more
|
|
@@ -1103,14 +1129,20 @@ class OTCS:
|
|
|
1103
1129
|
else:
|
|
1104
1130
|
return None
|
|
1105
1131
|
# end except Timeout
|
|
1106
|
-
except requests.exceptions.ConnectionError:
|
|
1132
|
+
except requests.exceptions.ConnectionError as connection_error:
|
|
1107
1133
|
if retries <= max_retries:
|
|
1108
1134
|
self.logger.warning(
|
|
1109
|
-
"
|
|
1110
|
-
|
|
1135
|
+
"Cannot connect to OTCS at -> %s; error -> %s! Retrying in %d seconds... %d/%d",
|
|
1136
|
+
url,
|
|
1137
|
+
str(connection_error),
|
|
1138
|
+
REQUEST_RETRY_DELAY,
|
|
1139
|
+
retries,
|
|
1140
|
+
max_retries,
|
|
1111
1141
|
)
|
|
1112
1142
|
retries += 1
|
|
1113
1143
|
|
|
1144
|
+
# The connection error could have been caused by a restart of the OTCS pod or services.
|
|
1145
|
+
# So we better check if OTCS is ready to receive requests again before retrying:
|
|
1114
1146
|
while not self.is_ready():
|
|
1115
1147
|
self.logger.warning(
|
|
1116
1148
|
"Content Server is not ready to receive requests. Waiting for state change in %d seconds...",
|
|
@@ -1120,8 +1152,9 @@ class OTCS:
|
|
|
1120
1152
|
|
|
1121
1153
|
else:
|
|
1122
1154
|
self.logger.error(
|
|
1123
|
-
"%s; connection error",
|
|
1155
|
+
"%s; connection error -> %s",
|
|
1124
1156
|
failure_message,
|
|
1157
|
+
str(connection_error),
|
|
1125
1158
|
)
|
|
1126
1159
|
if retry_forever:
|
|
1127
1160
|
# If it fails after REQUEST_MAX_RETRIES retries
|
|
@@ -1160,13 +1193,17 @@ class OTCS:
|
|
|
1160
1193
|
The response object delivered by the request call.
|
|
1161
1194
|
additional_error_message (str):
|
|
1162
1195
|
Custom error message to include in logs.
|
|
1163
|
-
show_error (bool):
|
|
1164
|
-
If True, logs an error. If False, logs a warning.
|
|
1196
|
+
show_error (bool, optional):
|
|
1197
|
+
If True, logs an error / raises an exception. If False, logs a warning.
|
|
1165
1198
|
|
|
1166
1199
|
Returns:
|
|
1167
1200
|
dict | None:
|
|
1168
1201
|
Parsed response as a dictionary, or None in case of an error.
|
|
1169
1202
|
|
|
1203
|
+
Raises:
|
|
1204
|
+
requests.exceptions.ConnectionError:
|
|
1205
|
+
If the response cannot be decoded as JSON.
|
|
1206
|
+
|
|
1170
1207
|
"""
|
|
1171
1208
|
|
|
1172
1209
|
if not response_object:
|
|
@@ -1191,12 +1228,13 @@ class OTCS:
|
|
|
1191
1228
|
exception,
|
|
1192
1229
|
)
|
|
1193
1230
|
if show_error:
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1231
|
+
# Raise ConnectionError instead of returning None
|
|
1232
|
+
raise requests.exceptions.ConnectionError(message) from exception
|
|
1233
|
+
self.logger.warning(message)
|
|
1197
1234
|
return None
|
|
1198
|
-
|
|
1199
|
-
|
|
1235
|
+
# end try-except block
|
|
1236
|
+
|
|
1237
|
+
return dict_object
|
|
1200
1238
|
|
|
1201
1239
|
# end method definition
|
|
1202
1240
|
|
|
@@ -1226,9 +1264,7 @@ class OTCS:
|
|
|
1226
1264
|
|
|
1227
1265
|
"""
|
|
1228
1266
|
|
|
1229
|
-
if not response:
|
|
1230
|
-
return None
|
|
1231
|
-
if "results" not in response:
|
|
1267
|
+
if not response or "results" not in response:
|
|
1232
1268
|
return None
|
|
1233
1269
|
|
|
1234
1270
|
results = response["results"]
|
|
@@ -1408,7 +1444,7 @@ class OTCS:
|
|
|
1408
1444
|
|
|
1409
1445
|
def get_result_value(
|
|
1410
1446
|
self,
|
|
1411
|
-
response: dict,
|
|
1447
|
+
response: dict | None,
|
|
1412
1448
|
key: str,
|
|
1413
1449
|
index: int = 0,
|
|
1414
1450
|
property_name: str = "properties",
|
|
@@ -1421,8 +1457,8 @@ class OTCS:
|
|
|
1421
1457
|
developer.opentext.com.
|
|
1422
1458
|
|
|
1423
1459
|
Args:
|
|
1424
|
-
response (dict):
|
|
1425
|
-
REST API response object.
|
|
1460
|
+
response (dict | None):
|
|
1461
|
+
REST API response object. None is also handled.
|
|
1426
1462
|
key (str):
|
|
1427
1463
|
Key to find (e.g., "id", "name").
|
|
1428
1464
|
index (int, optional):
|
|
@@ -1435,7 +1471,7 @@ class OTCS:
|
|
|
1435
1471
|
Whether an error or just a warning should be logged.
|
|
1436
1472
|
|
|
1437
1473
|
Returns:
|
|
1438
|
-
str:
|
|
1474
|
+
str | None:
|
|
1439
1475
|
Value of the item with the given key, or None if no value is found.
|
|
1440
1476
|
|
|
1441
1477
|
"""
|
|
@@ -1521,16 +1557,22 @@ class OTCS:
|
|
|
1521
1557
|
data = results[index]["data"]
|
|
1522
1558
|
if isinstance(data, dict):
|
|
1523
1559
|
# data is a dict - we don't need index value:
|
|
1524
|
-
properties = data
|
|
1560
|
+
properties = data.get(property_name)
|
|
1525
1561
|
elif isinstance(data, list):
|
|
1526
1562
|
# data is a list - this has typically just one item, so we use 0 as index
|
|
1527
|
-
properties = data[0]
|
|
1563
|
+
properties = data[0].get(property_name)
|
|
1528
1564
|
else:
|
|
1529
1565
|
self.logger.error(
|
|
1530
1566
|
"Data needs to be a list or dict but it is -> %s",
|
|
1531
1567
|
str(type(data)),
|
|
1532
1568
|
)
|
|
1533
1569
|
return None
|
|
1570
|
+
if not properties:
|
|
1571
|
+
self.logger.error(
|
|
1572
|
+
"No properties found in data -> %s",
|
|
1573
|
+
str(data),
|
|
1574
|
+
)
|
|
1575
|
+
return None
|
|
1534
1576
|
if key not in properties:
|
|
1535
1577
|
if show_error:
|
|
1536
1578
|
self.logger.error("Key -> '%s' is not in result properties!", key)
|
|
@@ -1669,8 +1711,8 @@ class OTCS:
|
|
|
1669
1711
|
Defaults to "data".
|
|
1670
1712
|
|
|
1671
1713
|
Returns:
|
|
1672
|
-
|
|
1673
|
-
|
|
1714
|
+
iter:
|
|
1715
|
+
Iterator object for iterating through the values.
|
|
1674
1716
|
|
|
1675
1717
|
"""
|
|
1676
1718
|
|
|
@@ -1683,9 +1725,9 @@ class OTCS:
|
|
|
1683
1725
|
# than creating a list with all values at once.
|
|
1684
1726
|
# This is especially important for large result sets.
|
|
1685
1727
|
yield from (
|
|
1686
|
-
item[data_name][property_name]
|
|
1728
|
+
item[data_name][property_name] if property_name else item[data_name]
|
|
1687
1729
|
for item in response["results"]
|
|
1688
|
-
if isinstance(item.get(data_name), dict) and property_name in item[data_name]
|
|
1730
|
+
if isinstance(item.get(data_name), dict) and (not property_name or property_name in item[data_name])
|
|
1689
1731
|
)
|
|
1690
1732
|
|
|
1691
1733
|
# end method definition
|
|
@@ -1835,6 +1877,31 @@ class OTCS:
|
|
|
1835
1877
|
|
|
1836
1878
|
request_url = self.config()["authenticationUrl"]
|
|
1837
1879
|
|
|
1880
|
+
if self._otds_token and not revalidate:
|
|
1881
|
+
self.logger.debug(
|
|
1882
|
+
"Requesting OTCS ticket with existing OTDS token; calling -> %s",
|
|
1883
|
+
request_url,
|
|
1884
|
+
)
|
|
1885
|
+
# Add the OTDS token to the request headers:
|
|
1886
|
+
request_header = REQUEST_FORM_HEADERS | {"Authorization": f"Bearer {self._otds_token}"}
|
|
1887
|
+
|
|
1888
|
+
try:
|
|
1889
|
+
response = requests.get(
|
|
1890
|
+
url=request_url,
|
|
1891
|
+
headers=request_header,
|
|
1892
|
+
timeout=10,
|
|
1893
|
+
)
|
|
1894
|
+
if response.ok:
|
|
1895
|
+
# read the ticket from the response header:
|
|
1896
|
+
otcs_ticket = response.headers.get("OTCSTicket")
|
|
1897
|
+
|
|
1898
|
+
except requests.exceptions.RequestException as exception:
|
|
1899
|
+
self.logger.warning(
|
|
1900
|
+
"Unable to connect to -> %s; error -> %s",
|
|
1901
|
+
request_url,
|
|
1902
|
+
str(exception),
|
|
1903
|
+
)
|
|
1904
|
+
|
|
1838
1905
|
if self._otds_ticket and not revalidate:
|
|
1839
1906
|
self.logger.debug(
|
|
1840
1907
|
"Requesting OTCS ticket with existing OTDS ticket; calling -> %s",
|
|
@@ -2138,7 +2205,8 @@ class OTCS:
|
|
|
2138
2205
|
"""Apply Content Server administration settings from XML file.
|
|
2139
2206
|
|
|
2140
2207
|
Args:
|
|
2141
|
-
xml_file_path (str):
|
|
2208
|
+
xml_file_path (str):
|
|
2209
|
+
The fully qualified path to the XML settings file.
|
|
2142
2210
|
|
|
2143
2211
|
Returns:
|
|
2144
2212
|
dict | None:
|
|
@@ -2178,7 +2246,7 @@ class OTCS:
|
|
|
2178
2246
|
headers=request_header,
|
|
2179
2247
|
files=llconfig_file,
|
|
2180
2248
|
timeout=None,
|
|
2181
|
-
success_message="Admin settings in file -> '{}' have been applied".format(
|
|
2249
|
+
success_message="Admin settings in file -> '{}' have been applied.".format(
|
|
2182
2250
|
xml_file_path,
|
|
2183
2251
|
),
|
|
2184
2252
|
failure_message="Failed to import settings file -> '{}'".format(
|
|
@@ -2733,7 +2801,13 @@ class OTCS:
|
|
|
2733
2801
|
|
|
2734
2802
|
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="get_current_user")
|
|
2735
2803
|
def get_current_user(self) -> dict | None:
|
|
2736
|
-
"""Get the current authenticated user.
|
|
2804
|
+
"""Get the current authenticated user.
|
|
2805
|
+
|
|
2806
|
+
Returns:
|
|
2807
|
+
dict | None:
|
|
2808
|
+
Information for the current (authenticated) user.
|
|
2809
|
+
|
|
2810
|
+
"""
|
|
2737
2811
|
|
|
2738
2812
|
request_url = self.config()["authenticationUrl"]
|
|
2739
2813
|
|
|
@@ -2749,7 +2823,7 @@ class OTCS:
|
|
|
2749
2823
|
method="GET",
|
|
2750
2824
|
headers=request_header,
|
|
2751
2825
|
timeout=None,
|
|
2752
|
-
failure_message="Failed to get current user
|
|
2826
|
+
failure_message="Failed to get current user",
|
|
2753
2827
|
)
|
|
2754
2828
|
|
|
2755
2829
|
# end method definition
|
|
@@ -2764,6 +2838,7 @@ class OTCS:
|
|
|
2764
2838
|
email: str,
|
|
2765
2839
|
title: str,
|
|
2766
2840
|
base_group: int,
|
|
2841
|
+
phone: str = "",
|
|
2767
2842
|
privileges: list | None = None,
|
|
2768
2843
|
user_type: int = 0,
|
|
2769
2844
|
) -> dict | None:
|
|
@@ -2784,7 +2859,9 @@ class OTCS:
|
|
|
2784
2859
|
The title of the user.
|
|
2785
2860
|
base_group (int):
|
|
2786
2861
|
The base group id of the user (e.g. department)
|
|
2787
|
-
|
|
2862
|
+
phone (str, optional):
|
|
2863
|
+
The business phone number of the user.
|
|
2864
|
+
privileges (list | None, optional):
|
|
2788
2865
|
Possible values are Login, Public Access, Content Manager,
|
|
2789
2866
|
Modify Users, Modify Groups, User Admin Rights,
|
|
2790
2867
|
Grant Discovery, System Admin Rights
|
|
@@ -2808,6 +2885,7 @@ class OTCS:
|
|
|
2808
2885
|
"first_name": first_name,
|
|
2809
2886
|
"last_name": last_name,
|
|
2810
2887
|
"business_email": email,
|
|
2888
|
+
"business_phone": phone,
|
|
2811
2889
|
"title": title,
|
|
2812
2890
|
"group_id": base_group,
|
|
2813
2891
|
"privilege_login": ("Login" in privileges),
|
|
@@ -2987,11 +3065,14 @@ class OTCS:
|
|
|
2987
3065
|
"""Update a user with a profile photo (which must be an existing node).
|
|
2988
3066
|
|
|
2989
3067
|
Args:
|
|
2990
|
-
user_id (int):
|
|
2991
|
-
|
|
3068
|
+
user_id (int):
|
|
3069
|
+
The ID of the user.
|
|
3070
|
+
photo_id (int):
|
|
3071
|
+
The node ID of the photo.
|
|
2992
3072
|
|
|
2993
3073
|
Returns:
|
|
2994
|
-
dict | None:
|
|
3074
|
+
dict | None:
|
|
3075
|
+
Node information or None if photo node is not found.
|
|
2995
3076
|
|
|
2996
3077
|
"""
|
|
2997
3078
|
|
|
@@ -3027,10 +3108,12 @@ class OTCS:
|
|
|
3027
3108
|
that was introduced with version 23.4.
|
|
3028
3109
|
|
|
3029
3110
|
Args:
|
|
3030
|
-
user_name (str):
|
|
3111
|
+
user_name (str):
|
|
3112
|
+
The user to test (login name) for proxy.
|
|
3031
3113
|
|
|
3032
3114
|
Returns:
|
|
3033
|
-
bool:
|
|
3115
|
+
bool:
|
|
3116
|
+
True is user is proxy of current user. False if not.
|
|
3034
3117
|
|
|
3035
3118
|
"""
|
|
3036
3119
|
|
|
@@ -3956,7 +4039,7 @@ class OTCS:
|
|
|
3956
4039
|
method="GET",
|
|
3957
4040
|
headers=request_header,
|
|
3958
4041
|
timeout=REQUEST_TIMEOUT,
|
|
3959
|
-
failure_message="Failed to get system usage privileges
|
|
4042
|
+
failure_message="Failed to get system usage privileges",
|
|
3960
4043
|
)
|
|
3961
4044
|
|
|
3962
4045
|
if response:
|
|
@@ -4160,7 +4243,7 @@ class OTCS:
|
|
|
4160
4243
|
method="GET",
|
|
4161
4244
|
headers=request_header,
|
|
4162
4245
|
timeout=REQUEST_TIMEOUT,
|
|
4163
|
-
failure_message="Failed to get system usage privileges
|
|
4246
|
+
failure_message="Failed to get system usage privileges",
|
|
4164
4247
|
)
|
|
4165
4248
|
|
|
4166
4249
|
if response:
|
|
@@ -4306,9 +4389,9 @@ class OTCS:
|
|
|
4306
4389
|
def get_node(
|
|
4307
4390
|
self,
|
|
4308
4391
|
node_id: int,
|
|
4309
|
-
fields:
|
|
4392
|
+
fields: str | list = "properties", # per default we just get the most important information
|
|
4310
4393
|
metadata: bool = False,
|
|
4311
|
-
timeout:
|
|
4394
|
+
timeout: float = REQUEST_TIMEOUT,
|
|
4312
4395
|
) -> dict | None:
|
|
4313
4396
|
"""Get a node based on the node ID.
|
|
4314
4397
|
|
|
@@ -4335,7 +4418,7 @@ class OTCS:
|
|
|
4335
4418
|
The metadata will be returned under `results.metadata`, `metadata_map`,
|
|
4336
4419
|
and `metadata_order`.
|
|
4337
4420
|
Defaults to False.
|
|
4338
|
-
timeout (
|
|
4421
|
+
timeout (float, optional):
|
|
4339
4422
|
Timeout for the request in seconds. Defaults to `REQUEST_TIMEOUT`.
|
|
4340
4423
|
|
|
4341
4424
|
Returns:
|
|
@@ -4986,9 +5069,9 @@ class OTCS:
|
|
|
4986
5069
|
show_hidden (bool, optional):
|
|
4987
5070
|
Whether to list hidden items. Defaults to False.
|
|
4988
5071
|
limit (int, optional):
|
|
4989
|
-
The maximum number of results to return. Defaults to 100.
|
|
5072
|
+
The maximum number of results to return (page size). Defaults to 100.
|
|
4990
5073
|
page (int, optional):
|
|
4991
|
-
The page of results to retrieve. Defaults to 1 (first page).
|
|
5074
|
+
The page of results to retrieve (page number). Defaults to 1 (first page).
|
|
4992
5075
|
fields (str | list, optional):
|
|
4993
5076
|
Which fields to retrieve.
|
|
4994
5077
|
This can have a significant impact on performance.
|
|
@@ -5282,7 +5365,7 @@ class OTCS:
|
|
|
5282
5365
|
# end method definition
|
|
5283
5366
|
|
|
5284
5367
|
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="lookup_node")
|
|
5285
|
-
def
|
|
5368
|
+
def lookup_nodes(
|
|
5286
5369
|
self,
|
|
5287
5370
|
parent_node_id: int,
|
|
5288
5371
|
category: str,
|
|
@@ -5290,7 +5373,7 @@ class OTCS:
|
|
|
5290
5373
|
value: str,
|
|
5291
5374
|
attribute_set: str | None = None,
|
|
5292
5375
|
) -> dict | None:
|
|
5293
|
-
"""Lookup
|
|
5376
|
+
"""Lookup nodes under a parent node that have a specified value in a category attribute.
|
|
5294
5377
|
|
|
5295
5378
|
Args:
|
|
5296
5379
|
parent_node_id (int):
|
|
@@ -5306,10 +5389,12 @@ class OTCS:
|
|
|
5306
5389
|
|
|
5307
5390
|
Returns:
|
|
5308
5391
|
dict | None:
|
|
5309
|
-
Node wrapped in dictionary with "results" key or None if the REST API fails.
|
|
5392
|
+
Node(s) wrapped in dictionary with "results" key or None if the REST API fails.
|
|
5310
5393
|
|
|
5311
5394
|
"""
|
|
5312
5395
|
|
|
5396
|
+
results = {"results": []}
|
|
5397
|
+
|
|
5313
5398
|
# get_subnodes_iterator() returns a python generator that we use for iterating over all nodes
|
|
5314
5399
|
# in an efficient way avoiding to retrieve all nodes at once (which could be a large number):
|
|
5315
5400
|
for node in self.get_subnodes_iterator(
|
|
@@ -5338,11 +5423,10 @@ class OTCS:
|
|
|
5338
5423
|
continue
|
|
5339
5424
|
category_key = next(iter(category_schema))
|
|
5340
5425
|
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
if not attribute_schema:
|
|
5426
|
+
# There can be multiple attributes with the same name in a category
|
|
5427
|
+
# if the category has sets:
|
|
5428
|
+
attribute_schemas = [cat_elem for cat_elem in category_schema.values() if cat_elem.get("name") == attribute]
|
|
5429
|
+
if not attribute_schemas:
|
|
5346
5430
|
self.logger.debug(
|
|
5347
5431
|
"Node -> '%s' (%s) does not have attribute -> '%s'. Skipping...",
|
|
5348
5432
|
node_name,
|
|
@@ -5350,69 +5434,81 @@ class OTCS:
|
|
|
5350
5434
|
attribute,
|
|
5351
5435
|
)
|
|
5352
5436
|
continue
|
|
5353
|
-
attribute_key = attribute_schema["key"]
|
|
5354
|
-
# Split the attribute key once (1) at the first underscore from the right.
|
|
5355
|
-
# rsplit delivers a list and [-1] delivers the last list item:
|
|
5356
|
-
attribute_id = attribute_key.rsplit("_", 1)[-1]
|
|
5357
|
-
|
|
5358
|
-
if attribute_set:
|
|
5359
|
-
set_schema = next(
|
|
5360
|
-
(
|
|
5361
|
-
cat_elem
|
|
5362
|
-
for cat_elem in category_schema.values()
|
|
5363
|
-
if cat_elem.get("name") == attribute_set and cat_elem.get("persona") == "set"
|
|
5364
|
-
),
|
|
5365
|
-
None,
|
|
5366
|
-
)
|
|
5367
|
-
if not set_schema:
|
|
5368
|
-
self.logger.debug(
|
|
5369
|
-
"Node -> '%s' (%s) does not have attribute set -> '%s'. Skipping...",
|
|
5370
|
-
node_name,
|
|
5371
|
-
node_id,
|
|
5372
|
-
attribute_set,
|
|
5373
|
-
)
|
|
5374
|
-
continue
|
|
5375
|
-
set_key = set_schema["key"]
|
|
5376
|
-
else:
|
|
5377
|
-
set_schema = None
|
|
5378
|
-
set_key = None
|
|
5379
5437
|
|
|
5380
|
-
|
|
5438
|
+
# Traverse the attribute schemas with the matching attribute name:
|
|
5439
|
+
for attribute_schema in attribute_schemas:
|
|
5440
|
+
attribute_key = attribute_schema["key"]
|
|
5441
|
+
# Split the attribute key once (1) at the first underscore from the right.
|
|
5442
|
+
# rsplit delivers a list and [-1] delivers the last list item:
|
|
5443
|
+
attribute_id = attribute_key.rsplit("_", 1)[-1]
|
|
5381
5444
|
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5445
|
+
if attribute_set: # is the attribute_set parameter provided?
|
|
5446
|
+
set_schema = next(
|
|
5447
|
+
(
|
|
5448
|
+
cat_elem
|
|
5449
|
+
for cat_elem in category_schema.values()
|
|
5450
|
+
if cat_elem.get("name") == attribute_set and cat_elem.get("persona") == "set"
|
|
5451
|
+
),
|
|
5452
|
+
None,
|
|
5453
|
+
)
|
|
5454
|
+
if not set_schema:
|
|
5455
|
+
self.logger.debug(
|
|
5456
|
+
"Node -> '%s' (%s) does not have attribute set -> '%s'. Skipping...",
|
|
5457
|
+
node_name,
|
|
5458
|
+
node_id,
|
|
5459
|
+
attribute_set,
|
|
5460
|
+
)
|
|
5461
|
+
continue
|
|
5462
|
+
set_key = set_schema["key"]
|
|
5463
|
+
else: # no attribute set value provided via the attribute_set parameter:
|
|
5464
|
+
if "_x_" in attribute_key:
|
|
5465
|
+
# The lookup does not include a set name but this attribute key
|
|
5466
|
+
# belongs to a set attribute - so we can skip it:
|
|
5467
|
+
continue
|
|
5468
|
+
set_schema = None
|
|
5469
|
+
set_key = None
|
|
5470
|
+
|
|
5471
|
+
prefix = set_key + "_" if set_key else category_key + "_"
|
|
5472
|
+
|
|
5473
|
+
data = node["data"]["categories"]
|
|
5474
|
+
for cat_data in data:
|
|
5475
|
+
if set_key:
|
|
5476
|
+
for i in range(1, int(set_schema["multi_value_length_max"])):
|
|
5477
|
+
key = prefix + str(i) + "_" + attribute_id
|
|
5478
|
+
attribute_value = cat_data.get(key)
|
|
5479
|
+
if not attribute_value:
|
|
5480
|
+
break
|
|
5481
|
+
# Is it a multi-value attribute (i.e. a list of values)?
|
|
5482
|
+
if isinstance(attribute_value, list):
|
|
5483
|
+
if value in attribute_value:
|
|
5484
|
+
# Create a "results" dict that is compatible with normal REST calls
|
|
5485
|
+
# to not break get_result_value() method that may be called on the result:
|
|
5486
|
+
results["results"].append(node)
|
|
5487
|
+
elif value == attribute_value:
|
|
5488
|
+
# Create a results dict that is compatible with normal REST calls
|
|
5489
|
+
# to not break get_result_value() method that may be called on the result:
|
|
5490
|
+
results["results"].append(node)
|
|
5491
|
+
# end if set_key
|
|
5492
|
+
else:
|
|
5493
|
+
key = prefix + attribute_id
|
|
5387
5494
|
attribute_value = cat_data.get(key)
|
|
5388
5495
|
if not attribute_value:
|
|
5389
|
-
|
|
5496
|
+
continue
|
|
5390
5497
|
# Is it a multi-value attribute (i.e. a list of values)?
|
|
5391
5498
|
if isinstance(attribute_value, list):
|
|
5392
5499
|
if value in attribute_value:
|
|
5393
5500
|
# Create a "results" dict that is compatible with normal REST calls
|
|
5394
5501
|
# to not break get_result_value() method that may be called on the result:
|
|
5395
|
-
|
|
5502
|
+
results["results"].append(node)
|
|
5503
|
+
# If not a multi-value attribute, check for equality:
|
|
5396
5504
|
elif value == attribute_value:
|
|
5397
5505
|
# Create a results dict that is compatible with normal REST calls
|
|
5398
5506
|
# to not break get_result_value() method that may be called on the result:
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
break
|
|
5405
|
-
if isinstance(attribute_value, list):
|
|
5406
|
-
if value in attribute_value:
|
|
5407
|
-
# Create a "results" dict that is compatible with normal REST calls
|
|
5408
|
-
# to not break get_result_value() method that may be called on the result:
|
|
5409
|
-
return {"results": node}
|
|
5410
|
-
elif value == attribute_value:
|
|
5411
|
-
# Create a results dict that is compatible with normal REST calls
|
|
5412
|
-
# to not break get_result_value() method that may be called on the result:
|
|
5413
|
-
return {"results": node}
|
|
5414
|
-
# end for cat_data, cat_schema in zip(data, schema)
|
|
5415
|
-
# end for node in nodes
|
|
5507
|
+
results["results"].append(node)
|
|
5508
|
+
# end if set_key ... else
|
|
5509
|
+
# end for cat_data in data:
|
|
5510
|
+
# end for attribute_schema in attribute_schemas:
|
|
5511
|
+
# end for node in self.get_subnodes_iterator()
|
|
5416
5512
|
|
|
5417
5513
|
self.logger.debug(
|
|
5418
5514
|
"Couldn't find a node with the value -> '%s' in the attribute -> '%s' of category -> '%s' in parent with node ID -> %d.",
|
|
@@ -5422,7 +5518,7 @@ class OTCS:
|
|
|
5422
5518
|
parent_node_id,
|
|
5423
5519
|
)
|
|
5424
5520
|
|
|
5425
|
-
return
|
|
5521
|
+
return results if results["results"] else None
|
|
5426
5522
|
|
|
5427
5523
|
# end method definition
|
|
5428
5524
|
|
|
@@ -6322,14 +6418,14 @@ class OTCS:
|
|
|
6322
6418
|
def get_volume(
|
|
6323
6419
|
self,
|
|
6324
6420
|
volume_type: int,
|
|
6325
|
-
timeout:
|
|
6421
|
+
timeout: float = REQUEST_TIMEOUT,
|
|
6326
6422
|
) -> dict | None:
|
|
6327
6423
|
"""Get Volume information based on the volume type ID.
|
|
6328
6424
|
|
|
6329
6425
|
Args:
|
|
6330
6426
|
volume_type (int):
|
|
6331
6427
|
The ID of the volume type.
|
|
6332
|
-
timeout (
|
|
6428
|
+
timeout (float | None, optional):
|
|
6333
6429
|
The timeout for the request in seconds.
|
|
6334
6430
|
|
|
6335
6431
|
Returns:
|
|
@@ -6592,6 +6688,7 @@ class OTCS:
|
|
|
6592
6688
|
external_modify_date: str | None = None,
|
|
6593
6689
|
external_create_date: str | None = None,
|
|
6594
6690
|
extract_zip: bool = False,
|
|
6691
|
+
replace_existing: bool = False,
|
|
6595
6692
|
show_error: bool = True,
|
|
6596
6693
|
) -> dict | None:
|
|
6597
6694
|
"""Fetch a file from a URL or local filesystem and uploads it to a OTCS parent.
|
|
@@ -6633,6 +6730,9 @@ class OTCS:
|
|
|
6633
6730
|
extract_zip (bool, optional):
|
|
6634
6731
|
If True, automatically extract ZIP files and upload extracted directory. If False,
|
|
6635
6732
|
upload the unchanged Zip file.
|
|
6733
|
+
replace_existing (bool, optional):
|
|
6734
|
+
If True, replaces an existing file with the same name in the target folder. If False,
|
|
6735
|
+
the upload will fail if a file with the same name already exists.
|
|
6636
6736
|
show_error (bool, optional):
|
|
6637
6737
|
If True, treats the upload failure as an error. If False, no error is shown (useful if the file already exists).
|
|
6638
6738
|
|
|
@@ -6683,8 +6783,7 @@ class OTCS:
|
|
|
6683
6783
|
file_content = response.content
|
|
6684
6784
|
|
|
6685
6785
|
# If path_or_url specifies a directory or a zip file we want to extract
|
|
6686
|
-
# it and then defer the upload to upload_directory_to_parent()
|
|
6687
|
-
|
|
6786
|
+
# it and then defer the upload to upload_directory_to_parent():
|
|
6688
6787
|
elif os.path.exists(file_url) and (
|
|
6689
6788
|
((file_url.endswith(".zip") or mime_type == "application/x-zip-compressed") and extract_zip)
|
|
6690
6789
|
or os.path.isdir(file_url)
|
|
@@ -6692,6 +6791,7 @@ class OTCS:
|
|
|
6692
6791
|
return self.upload_directory_to_parent(
|
|
6693
6792
|
parent_id=parent_id,
|
|
6694
6793
|
file_path=file_url,
|
|
6794
|
+
replace_existing=replace_existing,
|
|
6695
6795
|
)
|
|
6696
6796
|
|
|
6697
6797
|
elif os.path.exists(file_url):
|
|
@@ -6786,7 +6886,7 @@ class OTCS:
|
|
|
6786
6886
|
# end method definition
|
|
6787
6887
|
|
|
6788
6888
|
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="upload_directory_to_parent")
|
|
6789
|
-
def upload_directory_to_parent(self, parent_id: int, file_path: str) -> dict | None:
|
|
6889
|
+
def upload_directory_to_parent(self, parent_id: int, file_path: str, replace_existing: bool = True) -> dict | None:
|
|
6790
6890
|
"""Upload a directory or an uncompressed zip file to Content Server.
|
|
6791
6891
|
|
|
6792
6892
|
IMPORTANT: if the path ends in a file then we assume it is a ZIP file!
|
|
@@ -6796,6 +6896,8 @@ class OTCS:
|
|
|
6796
6896
|
ID of the parent in Content Server.
|
|
6797
6897
|
file_path (str):
|
|
6798
6898
|
File system path to the directory or zip file.
|
|
6899
|
+
replace_existing (bool, optional):
|
|
6900
|
+
If True, existing files are replaced by uploading a new version.
|
|
6799
6901
|
|
|
6800
6902
|
Returns:
|
|
6801
6903
|
dict | None:
|
|
@@ -6893,21 +6995,25 @@ class OTCS:
|
|
|
6893
6995
|
response = self.upload_directory_to_parent(
|
|
6894
6996
|
parent_id=current_parent_id,
|
|
6895
6997
|
file_path=full_file_path,
|
|
6998
|
+
replace_existing=replace_existing,
|
|
6896
6999
|
)
|
|
6897
7000
|
if response and not first_response:
|
|
6898
7001
|
first_response = response.copy()
|
|
6899
7002
|
continue
|
|
7003
|
+
# Check if the file already exists:
|
|
6900
7004
|
response = self.get_node_by_parent_and_name(
|
|
6901
7005
|
parent_id=current_parent_id,
|
|
6902
7006
|
name=file_name,
|
|
6903
7007
|
)
|
|
6904
7008
|
if not response or not response["results"]:
|
|
7009
|
+
# File does not yet exist - upload new document:
|
|
6905
7010
|
response = self.upload_file_to_parent(
|
|
6906
7011
|
parent_id=current_parent_id,
|
|
6907
7012
|
file_url=full_file_path,
|
|
6908
7013
|
file_name=file_name,
|
|
6909
7014
|
)
|
|
6910
|
-
|
|
7015
|
+
elif replace_existing:
|
|
7016
|
+
# Document does already exist - upload a new version if replace existing is requested:
|
|
6911
7017
|
existing_document_id = self.get_result_value(
|
|
6912
7018
|
response=response,
|
|
6913
7019
|
key="id",
|
|
@@ -7429,8 +7535,10 @@ class OTCS:
|
|
|
7429
7535
|
node_id: int,
|
|
7430
7536
|
file_path: str,
|
|
7431
7537
|
version_number: str | int = "",
|
|
7538
|
+
chunk_size: int = 8192,
|
|
7539
|
+
overwrite: bool = True,
|
|
7432
7540
|
) -> bool:
|
|
7433
|
-
"""Download a document from OTCS to local file system.
|
|
7541
|
+
"""Download a document (version) from OTCS to local file system.
|
|
7434
7542
|
|
|
7435
7543
|
Args:
|
|
7436
7544
|
node_id (int):
|
|
@@ -7440,6 +7548,12 @@ class OTCS:
|
|
|
7440
7548
|
version_number (str | int, optional):
|
|
7441
7549
|
The version of the document to download.
|
|
7442
7550
|
If version = "" then download the latest version.
|
|
7551
|
+
chunk_size (int, optional):
|
|
7552
|
+
The chunk size to use when downloading the document in bytes.
|
|
7553
|
+
Default is 8192 bytes.
|
|
7554
|
+
overwrite (bool, optional):
|
|
7555
|
+
If True, overwrite the file if it already exists. If False, do not overwrite
|
|
7556
|
+
and return False if the file already exists.
|
|
7443
7557
|
|
|
7444
7558
|
Returns:
|
|
7445
7559
|
bool:
|
|
@@ -7449,24 +7563,26 @@ class OTCS:
|
|
|
7449
7563
|
"""
|
|
7450
7564
|
|
|
7451
7565
|
if not version_number:
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7566
|
+
# we retrieve the latest version - using V1 REST API. V2 has issues with downloading files:
|
|
7567
|
+
request_url = self.config()["nodesUrl"] + "/" + str(node_id) + "/content"
|
|
7568
|
+
self.logger.debug(
|
|
7569
|
+
"Download document with node ID -> %d (latest version); calling -> %s",
|
|
7570
|
+
node_id,
|
|
7571
|
+
request_url,
|
|
7572
|
+
)
|
|
7573
|
+
else:
|
|
7574
|
+
# we retrieve the given version - using V1 REST API. V2 has issues with downloading files:
|
|
7575
|
+
request_url = (
|
|
7576
|
+
self.config()["nodesUrl"] + "/" + str(node_id) + "/versions/" + str(version_number) + "/content"
|
|
7577
|
+
)
|
|
7578
|
+
self.logger.debug(
|
|
7579
|
+
"Download document with node ID -> %d and version number -> %d; calling -> %s",
|
|
7580
|
+
node_id,
|
|
7581
|
+
version_number,
|
|
7582
|
+
request_url,
|
|
7583
|
+
)
|
|
7462
7584
|
request_header = self.request_download_header()
|
|
7463
7585
|
|
|
7464
|
-
self.logger.debug(
|
|
7465
|
-
"Download document with node ID -> %d; calling -> %s",
|
|
7466
|
-
node_id,
|
|
7467
|
-
request_url,
|
|
7468
|
-
)
|
|
7469
|
-
|
|
7470
7586
|
response = self.do_request(
|
|
7471
7587
|
url=request_url,
|
|
7472
7588
|
method="GET",
|
|
@@ -7482,6 +7598,26 @@ class OTCS:
|
|
|
7482
7598
|
if response is None:
|
|
7483
7599
|
return False
|
|
7484
7600
|
|
|
7601
|
+
total_size = int(response.headers["Content-Length"]) if "Content-Length" in response.headers else None
|
|
7602
|
+
|
|
7603
|
+
content_encoding = response.headers.get("Content-Encoding", "").lower()
|
|
7604
|
+
is_compressed = content_encoding in ("gzip", "deflate", "br")
|
|
7605
|
+
|
|
7606
|
+
self.logger.debug(
|
|
7607
|
+
"Downloading document with node ID -> %d to file -> '%s'; total size -> %s bytes; content encoding -> '%s'",
|
|
7608
|
+
node_id,
|
|
7609
|
+
file_path,
|
|
7610
|
+
total_size,
|
|
7611
|
+
content_encoding,
|
|
7612
|
+
)
|
|
7613
|
+
|
|
7614
|
+
if os.path.exists(file_path) and not overwrite:
|
|
7615
|
+
self.logger.warning(
|
|
7616
|
+
"File -> '%s' already exists and overwrite is set to False, not downloading document.",
|
|
7617
|
+
file_path,
|
|
7618
|
+
)
|
|
7619
|
+
return False
|
|
7620
|
+
|
|
7485
7621
|
directory = os.path.dirname(file_path)
|
|
7486
7622
|
if not os.path.exists(directory):
|
|
7487
7623
|
self.logger.info(
|
|
@@ -7490,12 +7626,31 @@ class OTCS:
|
|
|
7490
7626
|
)
|
|
7491
7627
|
os.makedirs(directory)
|
|
7492
7628
|
|
|
7629
|
+
bytes_downloaded = 0
|
|
7493
7630
|
try:
|
|
7494
7631
|
with open(file_path, "wb") as download_file:
|
|
7495
|
-
|
|
7496
|
-
|
|
7632
|
+
for chunk in response.iter_content(chunk_size=chunk_size):
|
|
7633
|
+
if chunk:
|
|
7634
|
+
download_file.write(chunk)
|
|
7635
|
+
bytes_downloaded += len(chunk)
|
|
7636
|
+
|
|
7637
|
+
except Exception as e:
|
|
7638
|
+
self.logger.error(
|
|
7639
|
+
"Error while writing content to file -> %s after %d bytes downloaded; error -> %s",
|
|
7640
|
+
file_path,
|
|
7641
|
+
bytes_downloaded,
|
|
7642
|
+
str(e),
|
|
7643
|
+
)
|
|
7644
|
+
return False
|
|
7645
|
+
|
|
7646
|
+
# if we have a total size and the content is not compressed
|
|
7647
|
+
# we can do a sanity check if the downloaded size matches
|
|
7648
|
+
# the expected size:
|
|
7649
|
+
if total_size and not is_compressed and bytes_downloaded != total_size:
|
|
7497
7650
|
self.logger.error(
|
|
7498
|
-
"
|
|
7651
|
+
"Downloaded size (%d bytes) does not match expected size (%d bytes) for file -> '%s'",
|
|
7652
|
+
bytes_downloaded,
|
|
7653
|
+
total_size,
|
|
7499
7654
|
file_path,
|
|
7500
7655
|
)
|
|
7501
7656
|
return False
|
|
@@ -7592,9 +7747,11 @@ class OTCS:
|
|
|
7592
7747
|
search_term: str,
|
|
7593
7748
|
look_for: str = "complexQuery",
|
|
7594
7749
|
modifier: str = "",
|
|
7750
|
+
within: str = "all",
|
|
7595
7751
|
slice_id: int = 0,
|
|
7596
7752
|
query_id: int = 0,
|
|
7597
7753
|
template_id: int = 0,
|
|
7754
|
+
location_id: int | None = None,
|
|
7598
7755
|
limit: int = 100,
|
|
7599
7756
|
page: int = 1,
|
|
7600
7757
|
) -> dict | None:
|
|
@@ -7619,12 +7776,20 @@ class OTCS:
|
|
|
7619
7776
|
- 'wordendswith'
|
|
7620
7777
|
If not specified or any value other than these options is given,
|
|
7621
7778
|
it is ignored.
|
|
7779
|
+
within (str, optional):
|
|
7780
|
+
The scope of the search. Possible values are:
|
|
7781
|
+
- 'all': search in content and in metadata (default)
|
|
7782
|
+
- 'content': search only in document content
|
|
7783
|
+
- 'metadata': search only in item metadata
|
|
7622
7784
|
slice_id (int, optional):
|
|
7623
7785
|
The ID of an existing search slice.
|
|
7624
7786
|
query_id (int, optional):
|
|
7625
7787
|
The ID of a saved search query.
|
|
7626
7788
|
template_id (int, optional):
|
|
7627
7789
|
The ID of a saved search template.
|
|
7790
|
+
location_id (int | None, optional):
|
|
7791
|
+
The ID of a folder or workspace to start a search from here.
|
|
7792
|
+
None = unrestricted search (default).
|
|
7628
7793
|
limit (int, optional):
|
|
7629
7794
|
The maximum number of results to return. Default is 100.
|
|
7630
7795
|
page (int, optional):
|
|
@@ -7809,6 +7974,7 @@ class OTCS:
|
|
|
7809
7974
|
search_post_body = {
|
|
7810
7975
|
"where": search_term,
|
|
7811
7976
|
"lookfor": look_for,
|
|
7977
|
+
"within": within,
|
|
7812
7978
|
"page": page,
|
|
7813
7979
|
"limit": limit,
|
|
7814
7980
|
}
|
|
@@ -7821,6 +7987,8 @@ class OTCS:
|
|
|
7821
7987
|
search_post_body["query_id"] = query_id
|
|
7822
7988
|
if template_id > 0:
|
|
7823
7989
|
search_post_body["template_id"] = template_id
|
|
7990
|
+
if location_id is not None:
|
|
7991
|
+
search_post_body["location_id1"] = location_id
|
|
7824
7992
|
|
|
7825
7993
|
request_url = self.config()["searchUrl"]
|
|
7826
7994
|
request_header = self.request_form_header()
|
|
@@ -7847,10 +8015,13 @@ class OTCS:
|
|
|
7847
8015
|
search_term: str,
|
|
7848
8016
|
look_for: str = "complexQuery",
|
|
7849
8017
|
modifier: str = "",
|
|
8018
|
+
within: str = "all",
|
|
7850
8019
|
slice_id: int = 0,
|
|
7851
8020
|
query_id: int = 0,
|
|
7852
8021
|
template_id: int = 0,
|
|
8022
|
+
location_id: int | None = None,
|
|
7853
8023
|
page_size: int = 100,
|
|
8024
|
+
limit: int | None = None,
|
|
7854
8025
|
) -> iter:
|
|
7855
8026
|
"""Get an iterator object to traverse all search results for a given search.
|
|
7856
8027
|
|
|
@@ -7878,19 +8049,31 @@ class OTCS:
|
|
|
7878
8049
|
Defines a modifier for the search. Possible values are:
|
|
7879
8050
|
- 'synonymsof', 'relatedto', 'soundslike', 'wordbeginswith', 'wordendswith'.
|
|
7880
8051
|
If not specified or any value other than these options is given, it is ignored.
|
|
8052
|
+
within (str, optional):
|
|
8053
|
+
The scope of the search. Possible values are:
|
|
8054
|
+
- 'all': search in content and in metadata (default)
|
|
8055
|
+
- 'content': search only in document content
|
|
8056
|
+
- 'metadata': search only in item metadata
|
|
7881
8057
|
slice_id (int, optional):
|
|
7882
8058
|
The ID of an existing search slice.
|
|
7883
8059
|
query_id (int, optional):
|
|
7884
8060
|
The ID of a saved search query.
|
|
7885
8061
|
template_id (int, optional):
|
|
7886
8062
|
The ID of a saved search template.
|
|
8063
|
+
location_id (int | None, optional):
|
|
8064
|
+
The ID of a folder or workspace to start a search from here.
|
|
8065
|
+
None = unrestricted search (default).
|
|
7887
8066
|
page_size (int, optional):
|
|
7888
8067
|
The maximum number of results to return. Default is 100.
|
|
7889
|
-
For the iterator this
|
|
8068
|
+
For the iterator this is basically the chunk size.
|
|
8069
|
+
limit (int | None = None), optional):
|
|
8070
|
+
The maximum number of results to return in total.
|
|
8071
|
+
If None (default) all results are returned.
|
|
8072
|
+
If a number is provided only up to this number of results is returned.
|
|
7890
8073
|
|
|
7891
8074
|
Returns:
|
|
7892
|
-
|
|
7893
|
-
The search response
|
|
8075
|
+
iter:
|
|
8076
|
+
The search response iterator object.
|
|
7894
8077
|
|
|
7895
8078
|
"""
|
|
7896
8079
|
|
|
@@ -7899,9 +8082,11 @@ class OTCS:
|
|
|
7899
8082
|
search_term=search_term,
|
|
7900
8083
|
look_for=look_for,
|
|
7901
8084
|
modifier=modifier,
|
|
8085
|
+
within=within,
|
|
7902
8086
|
slice_id=slice_id,
|
|
7903
8087
|
query_id=query_id,
|
|
7904
8088
|
template_id=template_id,
|
|
8089
|
+
location_id=location_id,
|
|
7905
8090
|
limit=1,
|
|
7906
8091
|
page=1,
|
|
7907
8092
|
)
|
|
@@ -7912,6 +8097,9 @@ class OTCS:
|
|
|
7912
8097
|
return
|
|
7913
8098
|
|
|
7914
8099
|
number_of_results = response["collection"]["paging"]["total_count"]
|
|
8100
|
+
if limit and number_of_results > limit:
|
|
8101
|
+
number_of_results = limit
|
|
8102
|
+
|
|
7915
8103
|
if not number_of_results:
|
|
7916
8104
|
self.logger.debug(
|
|
7917
8105
|
"Search -> '%s' does not have results! Cannot iterate over results.",
|
|
@@ -7934,9 +8122,11 @@ class OTCS:
|
|
|
7934
8122
|
search_term=search_term,
|
|
7935
8123
|
look_for=look_for,
|
|
7936
8124
|
modifier=modifier,
|
|
8125
|
+
within=within,
|
|
7937
8126
|
slice_id=slice_id,
|
|
7938
8127
|
query_id=query_id,
|
|
7939
8128
|
template_id=template_id,
|
|
8129
|
+
location_id=location_id,
|
|
7940
8130
|
limit=page_size,
|
|
7941
8131
|
page=page,
|
|
7942
8132
|
)
|
|
@@ -7980,7 +8170,7 @@ class OTCS:
|
|
|
7980
8170
|
request_header = self.cookie()
|
|
7981
8171
|
|
|
7982
8172
|
self.logger.debug(
|
|
7983
|
-
"Get external system connection -> %s; calling -> %s",
|
|
8173
|
+
"Get external system connection -> '%s'; calling -> %s",
|
|
7984
8174
|
connection_name,
|
|
7985
8175
|
request_url,
|
|
7986
8176
|
)
|
|
@@ -8170,7 +8360,7 @@ class OTCS:
|
|
|
8170
8360
|
# end method definition
|
|
8171
8361
|
|
|
8172
8362
|
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="deploy_workbench")
|
|
8173
|
-
def deploy_workbench(self, workbench_id: int) -> dict | None:
|
|
8363
|
+
def deploy_workbench(self, workbench_id: int) -> tuple[dict | None, int]:
|
|
8174
8364
|
"""Deploy an existing Workbench.
|
|
8175
8365
|
|
|
8176
8366
|
Args:
|
|
@@ -8180,6 +8370,8 @@ class OTCS:
|
|
|
8180
8370
|
Returns:
|
|
8181
8371
|
dict | None:
|
|
8182
8372
|
The deploy response or None if the deployment fails.
|
|
8373
|
+
int:
|
|
8374
|
+
Error count. Should be 0 if fully successful.
|
|
8183
8375
|
|
|
8184
8376
|
Example response:
|
|
8185
8377
|
{
|
|
@@ -8227,8 +8419,12 @@ class OTCS:
|
|
|
8227
8419
|
),
|
|
8228
8420
|
)
|
|
8229
8421
|
|
|
8422
|
+
# Transport packages canalso partly fail to deploy.
|
|
8423
|
+
# For such cases we determine the number of errors.
|
|
8424
|
+
error_count = 0
|
|
8425
|
+
|
|
8230
8426
|
if not response or "results" not in response:
|
|
8231
|
-
return None
|
|
8427
|
+
return (None, 0)
|
|
8232
8428
|
|
|
8233
8429
|
try:
|
|
8234
8430
|
error_count = response["results"]["data"]["status"]["error_count"]
|
|
@@ -8239,7 +8435,7 @@ class OTCS:
|
|
|
8239
8435
|
else:
|
|
8240
8436
|
success_count = response["results"]["data"]["status"]["success_count"]
|
|
8241
8437
|
self.logger.info(
|
|
8242
|
-
"Transport successfully deployed %d workbench items",
|
|
8438
|
+
"Transport successfully deployed %d workbench items.",
|
|
8243
8439
|
success_count,
|
|
8244
8440
|
)
|
|
8245
8441
|
|
|
@@ -8254,7 +8450,7 @@ class OTCS:
|
|
|
8254
8450
|
except Exception as e:
|
|
8255
8451
|
self.logger.debug(str(e))
|
|
8256
8452
|
|
|
8257
|
-
return response
|
|
8453
|
+
return (response, error_count)
|
|
8258
8454
|
|
|
8259
8455
|
# end method definition
|
|
8260
8456
|
|
|
@@ -8309,11 +8505,18 @@ class OTCS:
|
|
|
8309
8505
|
if extractions is None:
|
|
8310
8506
|
extractions = []
|
|
8311
8507
|
|
|
8508
|
+
while not self.is_ready():
|
|
8509
|
+
self.logger.info(
|
|
8510
|
+
"OTCS is not ready. Cannot deploy transport -> '%s' to OTCS. Waiting 30 seconds and retry...",
|
|
8511
|
+
package_name,
|
|
8512
|
+
)
|
|
8513
|
+
time.sleep(30)
|
|
8514
|
+
|
|
8312
8515
|
# Preparation: get volume IDs for Transport Warehouse (root volume and Transport Packages)
|
|
8313
8516
|
response = self.get_volume(volume_type=self.VOLUME_TYPE_TRANSPORT_WAREHOUSE)
|
|
8314
8517
|
transport_root_volume_id = self.get_result_value(response=response, key="id")
|
|
8315
8518
|
if not transport_root_volume_id:
|
|
8316
|
-
self.logger.error("Failed to retrieve transport root volume")
|
|
8519
|
+
self.logger.error("Failed to retrieve transport root volume!")
|
|
8317
8520
|
return None
|
|
8318
8521
|
self.logger.debug(
|
|
8319
8522
|
"Transport root volume ID -> %d",
|
|
@@ -8326,7 +8529,7 @@ class OTCS:
|
|
|
8326
8529
|
)
|
|
8327
8530
|
transport_package_volume_id = self.get_result_value(response=response, key="id")
|
|
8328
8531
|
if not transport_package_volume_id:
|
|
8329
|
-
self.logger.error("Failed to retrieve transport package volume")
|
|
8532
|
+
self.logger.error("Failed to retrieve transport package volume!")
|
|
8330
8533
|
return None
|
|
8331
8534
|
self.logger.debug(
|
|
8332
8535
|
"Transport package volume ID -> %d",
|
|
@@ -8345,20 +8548,20 @@ class OTCS:
|
|
|
8345
8548
|
package_id = self.get_result_value(response=response, key="id")
|
|
8346
8549
|
if package_id:
|
|
8347
8550
|
self.logger.debug(
|
|
8348
|
-
"Transport package -> '%s' does already exist; existing package ID -> %d",
|
|
8551
|
+
"Transport package -> '%s' does already exist; existing package ID -> %d.",
|
|
8349
8552
|
package_name,
|
|
8350
8553
|
package_id,
|
|
8351
8554
|
)
|
|
8352
8555
|
else:
|
|
8353
8556
|
self.logger.debug(
|
|
8354
|
-
"Transport package -> '%s' does not yet exist, loading from -> %s",
|
|
8557
|
+
"Transport package -> '%s' does not yet exist, loading from -> '%s'...",
|
|
8355
8558
|
package_name,
|
|
8356
8559
|
package_url,
|
|
8357
8560
|
)
|
|
8358
8561
|
# If we have string replacements configured execute them now:
|
|
8359
8562
|
if replacements:
|
|
8360
8563
|
self.logger.debug(
|
|
8361
|
-
"Transport -> '%s' has replacements -> %s",
|
|
8564
|
+
"Transport -> '%s' has replacements -> %s.",
|
|
8362
8565
|
package_name,
|
|
8363
8566
|
str(replacements),
|
|
8364
8567
|
)
|
|
@@ -8368,13 +8571,13 @@ class OTCS:
|
|
|
8368
8571
|
)
|
|
8369
8572
|
else:
|
|
8370
8573
|
self.logger.debug(
|
|
8371
|
-
"Transport -> '%s' has no replacements
|
|
8574
|
+
"Transport -> '%s' has no replacements.",
|
|
8372
8575
|
package_name,
|
|
8373
8576
|
)
|
|
8374
8577
|
# If we have data extractions configured execute them now:
|
|
8375
8578
|
if extractions:
|
|
8376
8579
|
self.logger.debug(
|
|
8377
|
-
"Transport -> '%s' has extractions -> %s",
|
|
8580
|
+
"Transport -> '%s' has extractions -> %s.",
|
|
8378
8581
|
package_name,
|
|
8379
8582
|
str(extractions),
|
|
8380
8583
|
)
|
|
@@ -8383,7 +8586,7 @@ class OTCS:
|
|
|
8383
8586
|
extractions=extractions,
|
|
8384
8587
|
)
|
|
8385
8588
|
else:
|
|
8386
|
-
self.logger.debug("Transport -> '%s' has no extractions
|
|
8589
|
+
self.logger.debug("Transport -> '%s' has no extractions.", package_name)
|
|
8387
8590
|
|
|
8388
8591
|
# Upload package to Transport Warehouse:
|
|
8389
8592
|
response = self.upload_file_to_volume(
|
|
@@ -8395,7 +8598,7 @@ class OTCS:
|
|
|
8395
8598
|
package_id = self.get_result_value(response=response, key="id")
|
|
8396
8599
|
if not package_id:
|
|
8397
8600
|
self.logger.error(
|
|
8398
|
-
"Failed to upload transport package -> %s",
|
|
8601
|
+
"Failed to upload transport package -> '%s'!",
|
|
8399
8602
|
package_url,
|
|
8400
8603
|
)
|
|
8401
8604
|
return None
|
|
@@ -8438,7 +8641,7 @@ class OTCS:
|
|
|
8438
8641
|
workbench_id = self.get_result_value(response=response, key="id")
|
|
8439
8642
|
if workbench_id:
|
|
8440
8643
|
self.logger.debug(
|
|
8441
|
-
"Workbench -> '%s' does already exist but is not successfully deployed; existing workbench ID -> %d",
|
|
8644
|
+
"Workbench -> '%s' does already exist but is not successfully deployed; existing workbench ID -> %d.",
|
|
8442
8645
|
workbench_name,
|
|
8443
8646
|
workbench_id,
|
|
8444
8647
|
)
|
|
@@ -8454,14 +8657,14 @@ class OTCS:
|
|
|
8454
8657
|
)
|
|
8455
8658
|
return None
|
|
8456
8659
|
self.logger.debug(
|
|
8457
|
-
"Successfully created workbench -> '%s'; new workbench ID -> %d",
|
|
8660
|
+
"Successfully created workbench -> '%s'; new workbench ID -> %d.",
|
|
8458
8661
|
workbench_name,
|
|
8459
8662
|
workbench_id,
|
|
8460
8663
|
)
|
|
8461
8664
|
|
|
8462
8665
|
# Step 3: Unpack Transport Package to Workbench
|
|
8463
8666
|
self.logger.debug(
|
|
8464
|
-
"Unpack transport package -> '%s' (%d) to workbench -> '%s' (%d)",
|
|
8667
|
+
"Unpack transport package -> '%s' (%d) to workbench -> '%s' (%d)...",
|
|
8465
8668
|
package_name,
|
|
8466
8669
|
package_id,
|
|
8467
8670
|
workbench_name,
|
|
@@ -8473,29 +8676,36 @@ class OTCS:
|
|
|
8473
8676
|
)
|
|
8474
8677
|
if not response:
|
|
8475
8678
|
self.logger.error(
|
|
8476
|
-
"Failed to unpack the transport package -> '%s'",
|
|
8679
|
+
"Failed to unpack the transport package -> '%s'!",
|
|
8477
8680
|
package_name,
|
|
8478
8681
|
)
|
|
8479
8682
|
return None
|
|
8480
8683
|
self.logger.debug(
|
|
8481
|
-
"Successfully unpackaged to workbench -> '%s' (%s)",
|
|
8684
|
+
"Successfully unpackaged to workbench -> '%s' (%s).",
|
|
8482
8685
|
workbench_name,
|
|
8483
8686
|
str(workbench_id),
|
|
8484
8687
|
)
|
|
8485
8688
|
|
|
8486
8689
|
# Step 4: Deploy Workbench
|
|
8487
8690
|
self.logger.debug(
|
|
8488
|
-
"Deploy workbench -> '%s' (%s)",
|
|
8691
|
+
"Deploy workbench -> '%s' (%s)...",
|
|
8489
8692
|
workbench_name,
|
|
8490
8693
|
str(workbench_id),
|
|
8491
8694
|
)
|
|
8492
|
-
response = self.deploy_workbench(workbench_id=workbench_id)
|
|
8493
|
-
if not response:
|
|
8494
|
-
self.logger.error(
|
|
8695
|
+
response, errors = self.deploy_workbench(workbench_id=workbench_id)
|
|
8696
|
+
if not response or errors > 0:
|
|
8697
|
+
self.logger.error(
|
|
8698
|
+
"Failed to deploy workbench -> '%s' (%s)!%s",
|
|
8699
|
+
workbench_name,
|
|
8700
|
+
str(workbench_id),
|
|
8701
|
+
" {} error{} occured during deployment.".format(errors, "s" if errors > 1 else "")
|
|
8702
|
+
if errors > 0
|
|
8703
|
+
else "",
|
|
8704
|
+
)
|
|
8495
8705
|
return None
|
|
8496
8706
|
|
|
8497
8707
|
self.logger.debug(
|
|
8498
|
-
"Successfully deployed workbench -> '%s' (%s)",
|
|
8708
|
+
"Successfully deployed workbench -> '%s' (%s).",
|
|
8499
8709
|
workbench_name,
|
|
8500
8710
|
str(workbench_id),
|
|
8501
8711
|
)
|
|
@@ -8589,7 +8799,7 @@ class OTCS:
|
|
|
8589
8799
|
)
|
|
8590
8800
|
continue
|
|
8591
8801
|
self.logger.debug(
|
|
8592
|
-
"Replace -> %s with -> %s in
|
|
8802
|
+
"Replace -> %s with -> %s in transport package -> %s",
|
|
8593
8803
|
replacement["placeholder"],
|
|
8594
8804
|
replacement["value"],
|
|
8595
8805
|
zip_file_folder,
|
|
@@ -8606,21 +8816,21 @@ class OTCS:
|
|
|
8606
8816
|
)
|
|
8607
8817
|
if found:
|
|
8608
8818
|
self.logger.debug(
|
|
8609
|
-
"Replacement -> %s has been completed successfully for
|
|
8819
|
+
"Replacement -> %s has been completed successfully for transport package -> '%s'.",
|
|
8610
8820
|
replacement,
|
|
8611
8821
|
zip_file_folder,
|
|
8612
8822
|
)
|
|
8613
8823
|
modified = True
|
|
8614
8824
|
else:
|
|
8615
8825
|
self.logger.warning(
|
|
8616
|
-
"Replacement -> %s not found in
|
|
8826
|
+
"Replacement -> %s not found in transport package -> '%s'!",
|
|
8617
8827
|
replacement,
|
|
8618
8828
|
zip_file_folder,
|
|
8619
8829
|
)
|
|
8620
8830
|
|
|
8621
8831
|
if not modified:
|
|
8622
8832
|
self.logger.warning(
|
|
8623
|
-
"None of the specified replacements have been found in
|
|
8833
|
+
"None of the specified replacements have been found in transport package -> %s. No need to create a new transport package.",
|
|
8624
8834
|
zip_file_folder,
|
|
8625
8835
|
)
|
|
8626
8836
|
return False
|
|
@@ -8628,7 +8838,7 @@ class OTCS:
|
|
|
8628
8838
|
# Create the new zip file and add all files from the directory to it
|
|
8629
8839
|
new_zip_file_path = os.path.dirname(zip_file_path) + "/new_" + os.path.basename(zip_file_path)
|
|
8630
8840
|
self.logger.debug(
|
|
8631
|
-
"Content of transport -> '%s' has been modified - repacking to new zip file -> %s",
|
|
8841
|
+
"Content of transport -> '%s' has been modified - repacking to new zip file -> '%s'...",
|
|
8632
8842
|
zip_file_folder,
|
|
8633
8843
|
new_zip_file_path,
|
|
8634
8844
|
)
|
|
@@ -8645,13 +8855,13 @@ class OTCS:
|
|
|
8645
8855
|
zip_ref.close()
|
|
8646
8856
|
old_zip_file_path = os.path.dirname(zip_file_path) + "/old_" + os.path.basename(zip_file_path)
|
|
8647
8857
|
self.logger.debug(
|
|
8648
|
-
"Rename orginal transport zip file -> '%s' to -> '%s'",
|
|
8858
|
+
"Rename orginal transport zip file -> '%s' to -> '%s'...",
|
|
8649
8859
|
zip_file_path,
|
|
8650
8860
|
old_zip_file_path,
|
|
8651
8861
|
)
|
|
8652
8862
|
os.rename(zip_file_path, old_zip_file_path)
|
|
8653
8863
|
self.logger.debug(
|
|
8654
|
-
"Rename new transport zip file -> '%s' to -> '%s'",
|
|
8864
|
+
"Rename new transport zip file -> '%s' to -> '%s'...",
|
|
8655
8865
|
new_zip_file_path,
|
|
8656
8866
|
zip_file_path,
|
|
8657
8867
|
)
|
|
@@ -8684,7 +8894,7 @@ class OTCS:
|
|
|
8684
8894
|
"""
|
|
8685
8895
|
|
|
8686
8896
|
if not os.path.isfile(zip_file_path):
|
|
8687
|
-
self.logger.error("Zip file -> '%s' not found
|
|
8897
|
+
self.logger.error("Zip file -> '%s' not found!", zip_file_path)
|
|
8688
8898
|
return False
|
|
8689
8899
|
|
|
8690
8900
|
# Extract the zip file to a temporary directory
|
|
@@ -8696,7 +8906,7 @@ class OTCS:
|
|
|
8696
8906
|
for extraction in extractions:
|
|
8697
8907
|
if "xpath" not in extraction:
|
|
8698
8908
|
self.logger.error(
|
|
8699
|
-
"Extraction needs an
|
|
8909
|
+
"Extraction needs an xpath but it is not specified! Skipping...",
|
|
8700
8910
|
)
|
|
8701
8911
|
continue
|
|
8702
8912
|
# Check if the extraction is explicitly disabled:
|
|
@@ -8709,7 +8919,7 @@ class OTCS:
|
|
|
8709
8919
|
|
|
8710
8920
|
xpath = extraction["xpath"]
|
|
8711
8921
|
self.logger.debug(
|
|
8712
|
-
"Using xpath -> %s to extract the data",
|
|
8922
|
+
"Using xpath -> %s to extract the data.",
|
|
8713
8923
|
xpath,
|
|
8714
8924
|
)
|
|
8715
8925
|
|
|
@@ -8721,7 +8931,7 @@ class OTCS:
|
|
|
8721
8931
|
)
|
|
8722
8932
|
if extracted_data:
|
|
8723
8933
|
self.logger.debug(
|
|
8724
|
-
"Extraction with
|
|
8934
|
+
"Extraction with xpath -> %s has been successfully completed for transport package -> '%s'.",
|
|
8725
8935
|
xpath,
|
|
8726
8936
|
zip_file_folder,
|
|
8727
8937
|
)
|
|
@@ -8729,7 +8939,7 @@ class OTCS:
|
|
|
8729
8939
|
extraction["data"] = extracted_data
|
|
8730
8940
|
else:
|
|
8731
8941
|
self.logger.warning(
|
|
8732
|
-
"Extraction with
|
|
8942
|
+
"Extraction with xpath -> %s has not delivered any data for transport package -> '%s'!",
|
|
8733
8943
|
xpath,
|
|
8734
8944
|
zip_file_folder,
|
|
8735
8945
|
)
|
|
@@ -8752,6 +8962,36 @@ class OTCS:
|
|
|
8752
8962
|
Business Object Types information (for all external systems)
|
|
8753
8963
|
or None if the request fails.
|
|
8754
8964
|
|
|
8965
|
+
Example:
|
|
8966
|
+
{
|
|
8967
|
+
'links': {
|
|
8968
|
+
'data': {
|
|
8969
|
+
'self': {
|
|
8970
|
+
'body': '',
|
|
8971
|
+
'content_type': '',
|
|
8972
|
+
'href': '/api/v2/businessobjecttypes',
|
|
8973
|
+
'method': 'GET',
|
|
8974
|
+
'name': ''
|
|
8975
|
+
}
|
|
8976
|
+
}
|
|
8977
|
+
},
|
|
8978
|
+
'results': [
|
|
8979
|
+
{
|
|
8980
|
+
'data': {
|
|
8981
|
+
'properties': {
|
|
8982
|
+
'bo_type': 'account',
|
|
8983
|
+
'bo_type_id': 54,
|
|
8984
|
+
'bo_type_name': 'gw.account',
|
|
8985
|
+
'ext_system_id': 'Guidewire Policy Center',
|
|
8986
|
+
'is_default_Search': True,
|
|
8987
|
+
'workspace_type_id': 33
|
|
8988
|
+
}
|
|
8989
|
+
}
|
|
8990
|
+
},
|
|
8991
|
+
...
|
|
8992
|
+
]
|
|
8993
|
+
}
|
|
8994
|
+
|
|
8755
8995
|
"""
|
|
8756
8996
|
|
|
8757
8997
|
request_url = self.config()["businessObjectTypesUrl"]
|
|
@@ -9247,7 +9487,7 @@ class OTCS:
|
|
|
9247
9487
|
expand_workspace_info: bool = True,
|
|
9248
9488
|
expand_templates: bool = True,
|
|
9249
9489
|
) -> dict | None:
|
|
9250
|
-
"""Get all workspace types configured in
|
|
9490
|
+
"""Get all workspace types configured in OTCS.
|
|
9251
9491
|
|
|
9252
9492
|
This REST API is very limited. It does not return all workspace type properties
|
|
9253
9493
|
you can see in OTCS business admin page.
|
|
@@ -9381,6 +9621,7 @@ class OTCS:
|
|
|
9381
9621
|
|
|
9382
9622
|
# end method definition
|
|
9383
9623
|
|
|
9624
|
+
@cache
|
|
9384
9625
|
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="get_workspace_type")
|
|
9385
9626
|
def get_workspace_type(
|
|
9386
9627
|
self,
|
|
@@ -9401,8 +9642,11 @@ class OTCS:
|
|
|
9401
9642
|
Workspace Types or None if the request fails.
|
|
9402
9643
|
|
|
9403
9644
|
Example:
|
|
9404
|
-
|
|
9405
|
-
|
|
9645
|
+
{
|
|
9646
|
+
'icon_url': '/cssupport/otsapxecm/wksp_contract_cust.png',
|
|
9647
|
+
'is_policies_enabled': False,
|
|
9648
|
+
'workspace_type': 'Sales Contract'
|
|
9649
|
+
}
|
|
9406
9650
|
|
|
9407
9651
|
"""
|
|
9408
9652
|
|
|
@@ -9424,11 +9668,11 @@ class OTCS:
|
|
|
9424
9668
|
|
|
9425
9669
|
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="get_workspace_type_name")
|
|
9426
9670
|
def get_workspace_type_name(self, type_id: int) -> str | None:
|
|
9427
|
-
"""Get the name of a workspace type based on the provided type ID.
|
|
9671
|
+
"""Get the name of a workspace type based on the provided workspace type ID.
|
|
9428
9672
|
|
|
9429
|
-
The name is taken from a OTCS
|
|
9673
|
+
The name is taken from a OTCS object variable self._workspace_type_lookup if recorded there.
|
|
9430
9674
|
If not yet derived it is determined via the REST API and then stored
|
|
9431
|
-
in
|
|
9675
|
+
in self._workspace_type_lookup (as a lookup cache).
|
|
9432
9676
|
|
|
9433
9677
|
Args:
|
|
9434
9678
|
type_id (int):
|
|
@@ -9439,6 +9683,10 @@ class OTCS:
|
|
|
9439
9683
|
The name of the workspace type. Or None if the type ID
|
|
9440
9684
|
was ot found.
|
|
9441
9685
|
|
|
9686
|
+
Side effects:
|
|
9687
|
+
Caches the workspace type name in self._workspace_type_lookup
|
|
9688
|
+
for future calls.
|
|
9689
|
+
|
|
9442
9690
|
"""
|
|
9443
9691
|
|
|
9444
9692
|
workspace_type = self._workspace_type_lookup.get(type_id)
|
|
@@ -9448,6 +9696,7 @@ class OTCS:
|
|
|
9448
9696
|
workspace_type = self.get_workspace_type(type_id=type_id)
|
|
9449
9697
|
type_name = workspace_type.get("workspace_type")
|
|
9450
9698
|
if type_name:
|
|
9699
|
+
# Update the lookup cache:
|
|
9451
9700
|
self._workspace_type_lookup[type_id] = {"location": None, "name": type_name}
|
|
9452
9701
|
return type_name
|
|
9453
9702
|
|
|
@@ -9455,6 +9704,43 @@ class OTCS:
|
|
|
9455
9704
|
|
|
9456
9705
|
# end method definition
|
|
9457
9706
|
|
|
9707
|
+
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="get_workspace_type_by_name")
|
|
9708
|
+
def get_workspace_type_names(self, lower_case: bool = False, renew: bool = False) -> list[str] | None:
|
|
9709
|
+
"""Get a list of all workspace type names.
|
|
9710
|
+
|
|
9711
|
+
Args:
|
|
9712
|
+
lower_case (bool):
|
|
9713
|
+
Whether to return the names in lower case.
|
|
9714
|
+
renew (bool):
|
|
9715
|
+
Whether to renew the cached workspace type names.
|
|
9716
|
+
|
|
9717
|
+
Returns:
|
|
9718
|
+
list[str] | None:
|
|
9719
|
+
List of workspace type names or None if the request fails.
|
|
9720
|
+
|
|
9721
|
+
Side effects:
|
|
9722
|
+
Caches the workspace type names in self._workspace_type_names
|
|
9723
|
+
for future calls.
|
|
9724
|
+
|
|
9725
|
+
"""
|
|
9726
|
+
|
|
9727
|
+
if self._workspace_type_names and not renew:
|
|
9728
|
+
return self._workspace_type_names
|
|
9729
|
+
|
|
9730
|
+
workspace_types = self.get_workspace_types_iterator()
|
|
9731
|
+
workspace_type_names = [
|
|
9732
|
+
self.get_result_value(response=workspace_type, key="wksp_type_name") for workspace_type in workspace_types
|
|
9733
|
+
]
|
|
9734
|
+
if lower_case:
|
|
9735
|
+
workspace_type_names = [name.lower() for name in workspace_type_names]
|
|
9736
|
+
|
|
9737
|
+
# Update the cache:
|
|
9738
|
+
self._workspace_type_names = workspace_type_names
|
|
9739
|
+
|
|
9740
|
+
return workspace_type_names
|
|
9741
|
+
|
|
9742
|
+
# end method definition
|
|
9743
|
+
|
|
9458
9744
|
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="get_workspace_templates")
|
|
9459
9745
|
def get_workspace_templates(
|
|
9460
9746
|
self, type_id: int | None = None, type_name: str | None = None
|
|
@@ -9580,7 +9866,7 @@ class OTCS:
|
|
|
9580
9866
|
def get_workspace(
|
|
9581
9867
|
self,
|
|
9582
9868
|
node_id: int,
|
|
9583
|
-
fields:
|
|
9869
|
+
fields: str | list = "properties", # per default we just get the most important information
|
|
9584
9870
|
metadata: bool = False,
|
|
9585
9871
|
) -> dict | None:
|
|
9586
9872
|
"""Get a workspace based on the node ID.
|
|
@@ -9594,11 +9880,11 @@ class OTCS:
|
|
|
9594
9880
|
Possible fields include:
|
|
9595
9881
|
- "properties" (can be further restricted by specifying sub-fields,
|
|
9596
9882
|
e.g., "properties{id,name,parent_id,description}")
|
|
9597
|
-
- "
|
|
9598
|
-
- "
|
|
9599
|
-
|
|
9600
|
-
- "
|
|
9601
|
-
|
|
9883
|
+
- "business_properties" (all the information for the business object and the external system)
|
|
9884
|
+
- "categories" (the category data of the workspace item)
|
|
9885
|
+
- "workspace_references" (a list with the references to business objects in external systems)
|
|
9886
|
+
- "display_urls" (a list with the URLs to external business systems)
|
|
9887
|
+
- "wksp_info" (currently just the icon information of the workspace)
|
|
9602
9888
|
This parameter can be a string to select one field group or a list of
|
|
9603
9889
|
strings to select multiple field groups.
|
|
9604
9890
|
Defaults to "properties".
|
|
@@ -9692,10 +9978,31 @@ class OTCS:
|
|
|
9692
9978
|
'wksp_type_name': 'Vendor',
|
|
9693
9979
|
'xgov_workspace_type': ''
|
|
9694
9980
|
}
|
|
9981
|
+
'display_urls': [
|
|
9982
|
+
{
|
|
9983
|
+
'business_object_type': 'LFA1',
|
|
9984
|
+
'business_object_type_id': 30,
|
|
9985
|
+
'business_object_type_name': 'Customer',
|
|
9986
|
+
'displayUrl': '/sap/bc/gui/sap/its/webgui?~logingroup=SPACE&~transaction=%2fOTX%2fRM_WSC_START_BO+KEY%3dpc%3AS3XljqcFfD0pakDIjUKul%3bOBJTYPE%3daccount&~OkCode=ONLI',
|
|
9987
|
+
'external_system_id': 'TE1',
|
|
9988
|
+
'external_system_name': 'SAP S/4HANA'
|
|
9989
|
+
}
|
|
9990
|
+
]
|
|
9695
9991
|
'wksp_info':
|
|
9696
9992
|
{
|
|
9697
9993
|
'wksp_type_icon': '/appimg/ot_bws/icons/16634%2Esvg?v=161194_13949'
|
|
9698
9994
|
}
|
|
9995
|
+
'workspace_references': [
|
|
9996
|
+
{
|
|
9997
|
+
'business_object_id': '0000010020',
|
|
9998
|
+
'business_object_type': 'LFA1',
|
|
9999
|
+
'business_object_type_id': 30,
|
|
10000
|
+
'external_system_id': 'TE1',
|
|
10001
|
+
'has_default_display': True,
|
|
10002
|
+
'has_default_search': True,
|
|
10003
|
+
'workspace_type_id': 37
|
|
10004
|
+
}
|
|
10005
|
+
]
|
|
9699
10006
|
},
|
|
9700
10007
|
'metadata': {...},
|
|
9701
10008
|
'metadata_order': {
|
|
@@ -9703,20 +10010,6 @@ class OTCS:
|
|
|
9703
10010
|
}
|
|
9704
10011
|
}
|
|
9705
10012
|
],
|
|
9706
|
-
'wksp_info': {
|
|
9707
|
-
'wksp_type_icon': '/appimg/ot_bws/icons/24643%2Esvg?v=161696_1252'
|
|
9708
|
-
}
|
|
9709
|
-
'workspace_references': [
|
|
9710
|
-
{
|
|
9711
|
-
'business_object_id': '0000010020',
|
|
9712
|
-
'business_object_type': 'LFA1',
|
|
9713
|
-
'business_object_type_id': 30,
|
|
9714
|
-
'external_system_id': 'TE1',
|
|
9715
|
-
'has_default_display': True,
|
|
9716
|
-
'has_default_search': True,
|
|
9717
|
-
'workspace_type_id': 37
|
|
9718
|
-
}
|
|
9719
|
-
]
|
|
9720
10013
|
}
|
|
9721
10014
|
```
|
|
9722
10015
|
|
|
@@ -9762,6 +10055,8 @@ class OTCS:
|
|
|
9762
10055
|
expanded_view: bool = True,
|
|
9763
10056
|
page: int | None = None,
|
|
9764
10057
|
limit: int | None = None,
|
|
10058
|
+
fields: str | list = "properties", # per default we just get the most important information
|
|
10059
|
+
metadata: bool = False,
|
|
9765
10060
|
) -> dict | None:
|
|
9766
10061
|
"""Get all workspace instances of a given type.
|
|
9767
10062
|
|
|
@@ -9794,6 +10089,25 @@ class OTCS:
|
|
|
9794
10089
|
The page to be returned (if more workspace instances exist
|
|
9795
10090
|
than given by the page limit).
|
|
9796
10091
|
The default is None.
|
|
10092
|
+
fields (str | list, optional):
|
|
10093
|
+
Which fields to retrieve. This can have a significant
|
|
10094
|
+
impact on performance.
|
|
10095
|
+
Possible fields include:
|
|
10096
|
+
- "properties" (can be further restricted by specifying sub-fields,
|
|
10097
|
+
e.g., "properties{id,name,parent_id,description}")
|
|
10098
|
+
- "categories"
|
|
10099
|
+
- "versions" (can be further restricted by specifying ".element(0)" to
|
|
10100
|
+
retrieve only the latest version)
|
|
10101
|
+
- "permissions" (can be further restricted by specifying ".limit(5)" to
|
|
10102
|
+
retrieve only the first 5 permissions)
|
|
10103
|
+
This parameter can be a string to select one field group or a list of
|
|
10104
|
+
strings to select multiple field groups.
|
|
10105
|
+
Defaults to "properties".
|
|
10106
|
+
metadata (bool, optional):
|
|
10107
|
+
Whether to return metadata (data type, field length, min/max values,...)
|
|
10108
|
+
about the data.
|
|
10109
|
+
Metadata will be returned under `results.metadata`, `metadata_map`,
|
|
10110
|
+
or `metadata_order`.
|
|
9797
10111
|
|
|
9798
10112
|
Returns:
|
|
9799
10113
|
dict | None:
|
|
@@ -9809,6 +10123,8 @@ class OTCS:
|
|
|
9809
10123
|
expanded_view=expanded_view,
|
|
9810
10124
|
page=page,
|
|
9811
10125
|
limit=limit,
|
|
10126
|
+
fields=fields,
|
|
10127
|
+
metadata=metadata,
|
|
9812
10128
|
)
|
|
9813
10129
|
|
|
9814
10130
|
# end method definition
|
|
@@ -9819,6 +10135,9 @@ class OTCS:
|
|
|
9819
10135
|
type_id: int | None = None,
|
|
9820
10136
|
expanded_view: bool = True,
|
|
9821
10137
|
page_size: int = 100,
|
|
10138
|
+
limit: int | None = None,
|
|
10139
|
+
fields: str | list = "properties", # per default we just get the most important information
|
|
10140
|
+
metadata: bool = False,
|
|
9822
10141
|
) -> iter:
|
|
9823
10142
|
"""Get an iterator object to traverse all workspace instances of a workspace type.
|
|
9824
10143
|
|
|
@@ -9849,6 +10168,29 @@ class OTCS:
|
|
|
9849
10168
|
page_size (int | None, optional):
|
|
9850
10169
|
The maximum number of workspace instances that should be delivered in one page.
|
|
9851
10170
|
The default is 100. If None is given then the internal OTCS limit seems to be 500.
|
|
10171
|
+
limit (int | None = None), optional):
|
|
10172
|
+
The maximum number of workspaces to return in total.
|
|
10173
|
+
If None (default) all workspaces are returned.
|
|
10174
|
+
If a number is provided only up to this number of results is returned.
|
|
10175
|
+
fields (str | list, optional):
|
|
10176
|
+
Which fields to retrieve. This can have a significant
|
|
10177
|
+
impact on performance.
|
|
10178
|
+
Possible fields include:
|
|
10179
|
+
- "properties" (can be further restricted by specifying sub-fields,
|
|
10180
|
+
e.g., "properties{id,name,parent_id,description}")
|
|
10181
|
+
- "categories"
|
|
10182
|
+
- "versions" (can be further restricted by specifying ".element(0)" to
|
|
10183
|
+
retrieve only the latest version)
|
|
10184
|
+
- "permissions" (can be further restricted by specifying ".limit(5)" to
|
|
10185
|
+
retrieve only the first 5 permissions)
|
|
10186
|
+
This parameter can be a string to select one field group or a list of
|
|
10187
|
+
strings to select multiple field groups.
|
|
10188
|
+
Defaults to "properties".
|
|
10189
|
+
metadata (bool, optional):
|
|
10190
|
+
Whether to return metadata (data type, field length, min/max values,...)
|
|
10191
|
+
about the data.
|
|
10192
|
+
Metadata will be returned under `results.metadata`, `metadata_map`,
|
|
10193
|
+
or `metadata_order`.
|
|
9852
10194
|
|
|
9853
10195
|
Returns:
|
|
9854
10196
|
iter:
|
|
@@ -9863,8 +10205,10 @@ class OTCS:
|
|
|
9863
10205
|
type_id=type_id,
|
|
9864
10206
|
name="",
|
|
9865
10207
|
expanded_view=expanded_view,
|
|
9866
|
-
page=1,
|
|
9867
10208
|
limit=1,
|
|
10209
|
+
page=1,
|
|
10210
|
+
fields=fields,
|
|
10211
|
+
metadata=metadata,
|
|
9868
10212
|
)
|
|
9869
10213
|
if not response or "results" not in response:
|
|
9870
10214
|
# Don't return None! Plain return is what we need for iterators.
|
|
@@ -9873,9 +10217,12 @@ class OTCS:
|
|
|
9873
10217
|
return
|
|
9874
10218
|
|
|
9875
10219
|
number_of_instances = response["paging"]["total_count"]
|
|
10220
|
+
if limit and number_of_instances > limit:
|
|
10221
|
+
number_of_instances = limit
|
|
10222
|
+
|
|
9876
10223
|
if not number_of_instances:
|
|
9877
10224
|
self.logger.debug(
|
|
9878
|
-
"Workspace type -> %s does not have instances! Cannot iterate over instances.",
|
|
10225
|
+
"Workspace type -> '%s' does not have instances! Cannot iterate over instances.",
|
|
9879
10226
|
type_name if type_name else str(type_id),
|
|
9880
10227
|
)
|
|
9881
10228
|
# Don't return None! Plain return is what we need for iterators.
|
|
@@ -9897,7 +10244,9 @@ class OTCS:
|
|
|
9897
10244
|
name="",
|
|
9898
10245
|
expanded_view=expanded_view,
|
|
9899
10246
|
page=page,
|
|
9900
|
-
limit=page_size,
|
|
10247
|
+
limit=page_size if limit is None else limit,
|
|
10248
|
+
fields=fields,
|
|
10249
|
+
metadata=metadata,
|
|
9901
10250
|
)
|
|
9902
10251
|
if not response or not response.get("results", None):
|
|
9903
10252
|
self.logger.warning(
|
|
@@ -9923,9 +10272,9 @@ class OTCS:
|
|
|
9923
10272
|
expanded_view: bool = True,
|
|
9924
10273
|
limit: int | None = None,
|
|
9925
10274
|
page: int | None = None,
|
|
9926
|
-
fields:
|
|
10275
|
+
fields: str | list = "properties", # per default we just get the most important information
|
|
9927
10276
|
metadata: bool = False,
|
|
9928
|
-
timeout:
|
|
10277
|
+
timeout: float = REQUEST_TIMEOUT,
|
|
9929
10278
|
) -> dict | None:
|
|
9930
10279
|
"""Lookup workspaces based on workspace type and workspace name.
|
|
9931
10280
|
|
|
@@ -9981,7 +10330,7 @@ class OTCS:
|
|
|
9981
10330
|
about the data.
|
|
9982
10331
|
Metadata will be returned under `results.metadata`, `metadata_map`,
|
|
9983
10332
|
or `metadata_order`.
|
|
9984
|
-
timeout (
|
|
10333
|
+
timeout (float, optional):
|
|
9985
10334
|
Specific timeout for the request in seconds. The default is the standard
|
|
9986
10335
|
timeout value REQUEST_TIMEOUT used by the OTCS module.
|
|
9987
10336
|
|
|
@@ -10191,7 +10540,7 @@ class OTCS:
|
|
|
10191
10540
|
external_system_name: str,
|
|
10192
10541
|
business_object_type: str,
|
|
10193
10542
|
business_object_id: str,
|
|
10194
|
-
|
|
10543
|
+
metadata: bool = False,
|
|
10195
10544
|
show_error: bool = False,
|
|
10196
10545
|
) -> dict | None:
|
|
10197
10546
|
"""Get a workspace based on the business object of an external system.
|
|
@@ -10203,7 +10552,7 @@ class OTCS:
|
|
|
10203
10552
|
Type of the Business object, e.g. KNA1 for SAP customers
|
|
10204
10553
|
business_object_id (str):
|
|
10205
10554
|
ID of the business object in the external system
|
|
10206
|
-
|
|
10555
|
+
metadata (bool, optional):
|
|
10207
10556
|
Whether or not workspace metadata (categories) should be returned.
|
|
10208
10557
|
Default is False.
|
|
10209
10558
|
show_error (bool, optional):
|
|
@@ -10285,7 +10634,7 @@ class OTCS:
|
|
|
10285
10634
|
+ "/boids/"
|
|
10286
10635
|
+ business_object_id
|
|
10287
10636
|
)
|
|
10288
|
-
if
|
|
10637
|
+
if metadata:
|
|
10289
10638
|
request_url += "?metadata"
|
|
10290
10639
|
|
|
10291
10640
|
request_header = self.request_form_header()
|
|
@@ -10319,7 +10668,7 @@ class OTCS:
|
|
|
10319
10668
|
# end method definition
|
|
10320
10669
|
|
|
10321
10670
|
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="lookup_workspace")
|
|
10322
|
-
def
|
|
10671
|
+
def lookup_workspaces(
|
|
10323
10672
|
self,
|
|
10324
10673
|
type_name: str,
|
|
10325
10674
|
category: str,
|
|
@@ -10327,11 +10676,12 @@ class OTCS:
|
|
|
10327
10676
|
value: str,
|
|
10328
10677
|
attribute_set: str | None = None,
|
|
10329
10678
|
) -> dict | None:
|
|
10330
|
-
"""Lookup
|
|
10679
|
+
"""Lookup workspaces that have a specified value in a category attribute.
|
|
10331
10680
|
|
|
10332
10681
|
Args:
|
|
10333
10682
|
type_name (str):
|
|
10334
|
-
The name of the workspace type.
|
|
10683
|
+
The name of the workspace type. This is required to determine
|
|
10684
|
+
the parent folder in which the workspaces of this type reside.
|
|
10335
10685
|
category (str):
|
|
10336
10686
|
The name of the category.
|
|
10337
10687
|
attribute (str):
|
|
@@ -10339,7 +10689,8 @@ class OTCS:
|
|
|
10339
10689
|
value (str):
|
|
10340
10690
|
The lookup value that is matched agains the node attribute value.
|
|
10341
10691
|
attribute_set (str | None, optional):
|
|
10342
|
-
The name of the attribute set
|
|
10692
|
+
The name of the attribute set. If None (default) the attribute to lookup
|
|
10693
|
+
is supposed to be a top-level attribute.
|
|
10343
10694
|
|
|
10344
10695
|
Returns:
|
|
10345
10696
|
dict | None:
|
|
@@ -10358,12 +10709,44 @@ class OTCS:
|
|
|
10358
10709
|
)
|
|
10359
10710
|
return None
|
|
10360
10711
|
|
|
10361
|
-
return self.
|
|
10712
|
+
return self.lookup_nodes(
|
|
10362
10713
|
parent_node_id=parent_id, category=category, attribute=attribute, value=value, attribute_set=attribute_set
|
|
10363
10714
|
)
|
|
10364
10715
|
|
|
10365
10716
|
# end method definition
|
|
10366
10717
|
|
|
10718
|
+
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="get_workspace")
|
|
10719
|
+
def get_workspace_references(
|
|
10720
|
+
self,
|
|
10721
|
+
node_id: int,
|
|
10722
|
+
) -> list | None:
|
|
10723
|
+
"""Get a workspace rewferences to business objects in external systems.
|
|
10724
|
+
|
|
10725
|
+
Args:
|
|
10726
|
+
node_id (int):
|
|
10727
|
+
The node ID of the workspace to retrieve.
|
|
10728
|
+
|
|
10729
|
+
Returns:
|
|
10730
|
+
list | None:
|
|
10731
|
+
A List of references to business objects in external systems.
|
|
10732
|
+
|
|
10733
|
+
"""
|
|
10734
|
+
|
|
10735
|
+
response = self.get_workspace(node_id=node_id, fields="workspace_references")
|
|
10736
|
+
|
|
10737
|
+
results = response.get("results")
|
|
10738
|
+
if not results:
|
|
10739
|
+
return None
|
|
10740
|
+
data = results.get("data")
|
|
10741
|
+
if not data:
|
|
10742
|
+
return None
|
|
10743
|
+
|
|
10744
|
+
workspace_references: list = data.get("workspace_references")
|
|
10745
|
+
|
|
10746
|
+
return workspace_references
|
|
10747
|
+
|
|
10748
|
+
# end method definition
|
|
10749
|
+
|
|
10367
10750
|
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="set_workspace_reference")
|
|
10368
10751
|
def set_workspace_reference(
|
|
10369
10752
|
self,
|
|
@@ -10422,13 +10805,88 @@ class OTCS:
|
|
|
10422
10805
|
headers=request_header,
|
|
10423
10806
|
data=workspace_put_data,
|
|
10424
10807
|
timeout=None,
|
|
10425
|
-
warning_message="Cannot update reference for workspace ID -> {} with business object connection -> ({}, {}, {})".format(
|
|
10808
|
+
warning_message="Cannot update reference for workspace ID -> {} with business object connection -> ('{}', '{}', {})".format(
|
|
10809
|
+
workspace_id,
|
|
10810
|
+
external_system_id,
|
|
10811
|
+
bo_type,
|
|
10812
|
+
bo_id,
|
|
10813
|
+
),
|
|
10814
|
+
failure_message="Failed to update reference for workspace ID -> {} with business object connection -> ('{}', '{}', {})".format(
|
|
10815
|
+
workspace_id,
|
|
10816
|
+
external_system_id,
|
|
10817
|
+
bo_type,
|
|
10818
|
+
bo_id,
|
|
10819
|
+
),
|
|
10820
|
+
show_error=show_error,
|
|
10821
|
+
)
|
|
10822
|
+
|
|
10823
|
+
# end method definition
|
|
10824
|
+
|
|
10825
|
+
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="delete_workspace_reference")
|
|
10826
|
+
def delete_workspace_reference(
|
|
10827
|
+
self,
|
|
10828
|
+
workspace_id: int,
|
|
10829
|
+
external_system_id: str | None = None,
|
|
10830
|
+
bo_type: str | None = None,
|
|
10831
|
+
bo_id: str | None = None,
|
|
10832
|
+
show_error: bool = True,
|
|
10833
|
+
) -> dict | None:
|
|
10834
|
+
"""Delete reference of workspace to a business object in an external system.
|
|
10835
|
+
|
|
10836
|
+
Args:
|
|
10837
|
+
workspace_id (int):
|
|
10838
|
+
The ID of the workspace.
|
|
10839
|
+
external_system_id (str | None, optional):
|
|
10840
|
+
Identifier of the external system (None if no external system).
|
|
10841
|
+
bo_type (str | None, optional):
|
|
10842
|
+
Business object type (None if no external system)
|
|
10843
|
+
bo_id (str | None, optional):
|
|
10844
|
+
Business object identifier / key (None if no external system)
|
|
10845
|
+
show_error (bool, optional):
|
|
10846
|
+
Log an error if workspace cration fails. Otherwise log a warning.
|
|
10847
|
+
|
|
10848
|
+
Returns:
|
|
10849
|
+
Request response or None in case of an error.
|
|
10850
|
+
|
|
10851
|
+
"""
|
|
10852
|
+
|
|
10853
|
+
request_url = self.config()["businessWorkspacesUrl"] + "/" + str(workspace_id) + "/workspacereferences"
|
|
10854
|
+
request_header = self.request_form_header()
|
|
10855
|
+
|
|
10856
|
+
if not external_system_id or not bo_type or not bo_id:
|
|
10857
|
+
self.logger.error(
|
|
10858
|
+
"Cannot update workspace reference - required Business Object information is missing!",
|
|
10859
|
+
)
|
|
10860
|
+
return None
|
|
10861
|
+
|
|
10862
|
+
self.logger.debug(
|
|
10863
|
+
"Delete workspace reference of workspace ID -> %d with business object connection -> (%s, %s, %s); calling -> %s",
|
|
10864
|
+
workspace_id,
|
|
10865
|
+
external_system_id,
|
|
10866
|
+
bo_type,
|
|
10867
|
+
bo_id,
|
|
10868
|
+
request_url,
|
|
10869
|
+
)
|
|
10870
|
+
|
|
10871
|
+
workspace_put_data = {
|
|
10872
|
+
"ext_system_id": external_system_id,
|
|
10873
|
+
"bo_type": bo_type,
|
|
10874
|
+
"bo_id": bo_id,
|
|
10875
|
+
}
|
|
10876
|
+
|
|
10877
|
+
return self.do_request(
|
|
10878
|
+
url=request_url,
|
|
10879
|
+
method="DELETE",
|
|
10880
|
+
headers=request_header,
|
|
10881
|
+
data=workspace_put_data,
|
|
10882
|
+
timeout=None,
|
|
10883
|
+
warning_message="Cannot delete reference for workspace ID -> {} with business object connection -> ({}, {}, {})".format(
|
|
10426
10884
|
workspace_id,
|
|
10427
10885
|
external_system_id,
|
|
10428
10886
|
bo_type,
|
|
10429
10887
|
bo_id,
|
|
10430
10888
|
),
|
|
10431
|
-
failure_message="Failed to
|
|
10889
|
+
failure_message="Failed to delete reference for workspace ID -> {} with business object connection -> ({}, {}, {})".format(
|
|
10432
10890
|
workspace_id,
|
|
10433
10891
|
external_system_id,
|
|
10434
10892
|
bo_type,
|
|
@@ -10475,26 +10933,26 @@ class OTCS:
|
|
|
10475
10933
|
A description of the new workspace.
|
|
10476
10934
|
workspace_type (int):
|
|
10477
10935
|
Type ID of the workspace, indicating its category or function.
|
|
10478
|
-
category_data (dict, optional):
|
|
10936
|
+
category_data (dict | None, optional):
|
|
10479
10937
|
Category and attribute data for the workspace.
|
|
10480
|
-
classifications (list):
|
|
10938
|
+
classifications (list | None, optional):
|
|
10481
10939
|
List of classification item IDs to apply to the new item.
|
|
10482
|
-
external_system_id (str, optional):
|
|
10940
|
+
external_system_id (str | None, optional):
|
|
10483
10941
|
External system identifier if linking the workspace to an external system.
|
|
10484
|
-
bo_type (str, optional):
|
|
10942
|
+
bo_type (str | None, optional):
|
|
10485
10943
|
Business object type, used if linking to an external system.
|
|
10486
|
-
bo_id (str, optional):
|
|
10944
|
+
bo_id (str | None, optional):
|
|
10487
10945
|
Business object identifier or key, used if linking to an external system.
|
|
10488
|
-
parent_id (int, optional):
|
|
10946
|
+
parent_id (int | None, optional):
|
|
10489
10947
|
ID of the parent workspace, required in special cases such as
|
|
10490
10948
|
sub-workspaces or location ambiguity.
|
|
10491
|
-
ibo_workspace_id (int, optional):
|
|
10949
|
+
ibo_workspace_id (int | None, optional):
|
|
10492
10950
|
ID of an existing workspace that is already linked to an external system.
|
|
10493
10951
|
Allows connecting multiple business objects (IBO).
|
|
10494
|
-
external_create_date (str, optional):
|
|
10952
|
+
external_create_date (str | None, optional):
|
|
10495
10953
|
Date of creation in the external system (format: YYYY-MM-DD).
|
|
10496
10954
|
None is the default.
|
|
10497
|
-
external_modify_date (str, optional):
|
|
10955
|
+
external_modify_date (str | None, optional):
|
|
10498
10956
|
Date of last modification in the external system (format: YYYY-MM-DD).
|
|
10499
10957
|
None is the default.
|
|
10500
10958
|
show_error (bool, optional):
|
|
@@ -10670,16 +11128,16 @@ class OTCS:
|
|
|
10670
11128
|
category_data (dict | None, optional):
|
|
10671
11129
|
Category and attribute data.
|
|
10672
11130
|
Default is None (attributes remain unchanged).
|
|
10673
|
-
external_system_id (str, optional):
|
|
11131
|
+
external_system_id (str | None, optional):
|
|
10674
11132
|
Identifier of the external system (None if no external system)
|
|
10675
|
-
bo_type (str, optional):
|
|
11133
|
+
bo_type (str | None, optional):
|
|
10676
11134
|
Business object type (None if no external system)
|
|
10677
|
-
bo_id (str, optional):
|
|
11135
|
+
bo_id (str | None, optional):
|
|
10678
11136
|
Business object identifier / key (None if no external system)
|
|
10679
|
-
external_create_date (str, optional):
|
|
11137
|
+
external_create_date (str | None, optional):
|
|
10680
11138
|
Date of creation in the external system
|
|
10681
11139
|
(format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS).
|
|
10682
|
-
external_modify_date (str, optional):
|
|
11140
|
+
external_modify_date (str | None, optional):
|
|
10683
11141
|
Date of last modification in the external system
|
|
10684
11142
|
(format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS).
|
|
10685
11143
|
show_error (bool, optional):
|
|
@@ -10787,12 +11245,13 @@ class OTCS:
|
|
|
10787
11245
|
def get_workspace_relationships(
|
|
10788
11246
|
self,
|
|
10789
11247
|
workspace_id: int,
|
|
10790
|
-
relationship_type: str | list
|
|
11248
|
+
relationship_type: str | list = "child",
|
|
10791
11249
|
related_workspace_name: str | None = None,
|
|
10792
11250
|
related_workspace_type_id: int | list | None = None,
|
|
10793
11251
|
limit: int | None = None,
|
|
10794
11252
|
page: int | None = None,
|
|
10795
|
-
fields:
|
|
11253
|
+
fields: str | list = "properties", # per default we just get the most important information
|
|
11254
|
+
metadata: bool = False,
|
|
10796
11255
|
) -> dict | None:
|
|
10797
11256
|
"""Get the Workspace relationships to other workspaces.
|
|
10798
11257
|
|
|
@@ -10804,7 +11263,7 @@ class OTCS:
|
|
|
10804
11263
|
workspace_id (int):
|
|
10805
11264
|
The ID of the workspace.
|
|
10806
11265
|
relationship_type (str | list, optional):
|
|
10807
|
-
Either "parent" or "child" (
|
|
11266
|
+
Either "parent" or "child" ("child" is the default).
|
|
10808
11267
|
If both ("child" and "parent") are requested then use a
|
|
10809
11268
|
list like ["child", "parent"].
|
|
10810
11269
|
related_workspace_name (str | None, optional):
|
|
@@ -10834,6 +11293,9 @@ class OTCS:
|
|
|
10834
11293
|
This parameter can be a string to select one field group or a list of
|
|
10835
11294
|
strings to select multiple field groups.
|
|
10836
11295
|
Defaults to "properties".
|
|
11296
|
+
metadata (bool, optional):
|
|
11297
|
+
Whether or not workspace metadata (categories) should be returned.
|
|
11298
|
+
Default is False.
|
|
10837
11299
|
|
|
10838
11300
|
Returns:
|
|
10839
11301
|
dict | None:
|
|
@@ -10928,7 +11390,13 @@ class OTCS:
|
|
|
10928
11390
|
if isinstance(relationship_type, str):
|
|
10929
11391
|
query["where_relationtype"] = relationship_type
|
|
10930
11392
|
elif isinstance(relationship_type, list):
|
|
10931
|
-
|
|
11393
|
+
if any(rt not in ["parent", "child"] for rt in relationship_type):
|
|
11394
|
+
self.logger.error(
|
|
11395
|
+
"Illegal relationship type for related workspace type! Must be either 'parent' or 'child'. -> %s",
|
|
11396
|
+
relationship_type,
|
|
11397
|
+
)
|
|
11398
|
+
return None
|
|
11399
|
+
query["where_rel_types"] = "{'" + ("','").join(relationship_type) + "'}"
|
|
10932
11400
|
else:
|
|
10933
11401
|
self.logger.error(
|
|
10934
11402
|
"Illegal relationship type for related workspace type!",
|
|
@@ -10956,6 +11424,8 @@ class OTCS:
|
|
|
10956
11424
|
|
|
10957
11425
|
encoded_query = urllib.parse.urlencode(query=query, doseq=False)
|
|
10958
11426
|
request_url += "?{}".format(encoded_query)
|
|
11427
|
+
if metadata:
|
|
11428
|
+
request_url += "&metadata"
|
|
10959
11429
|
|
|
10960
11430
|
request_header = self.request_form_header()
|
|
10961
11431
|
|
|
@@ -10980,11 +11450,13 @@ class OTCS:
|
|
|
10980
11450
|
def get_workspace_relationships_iterator(
|
|
10981
11451
|
self,
|
|
10982
11452
|
workspace_id: int,
|
|
10983
|
-
relationship_type: str | list
|
|
11453
|
+
relationship_type: str | list = "child",
|
|
10984
11454
|
related_workspace_name: str | None = None,
|
|
10985
11455
|
related_workspace_type_id: int | list | None = None,
|
|
10986
|
-
fields:
|
|
11456
|
+
fields: str | list = "properties", # per default we just get the most important information
|
|
10987
11457
|
page_size: int = 100,
|
|
11458
|
+
limit: int | None = None,
|
|
11459
|
+
metadata: bool = False,
|
|
10988
11460
|
) -> iter:
|
|
10989
11461
|
"""Get an iterator object to traverse all related workspaces for a workspace.
|
|
10990
11462
|
|
|
@@ -11005,7 +11477,7 @@ class OTCS:
|
|
|
11005
11477
|
workspace_id (int):
|
|
11006
11478
|
The ID of the workspace.
|
|
11007
11479
|
relationship_type (str | list, optional):
|
|
11008
|
-
Either "parent" or "child" (
|
|
11480
|
+
Either "parent" or "child" ("child" is the default).
|
|
11009
11481
|
If both ("child" and "parent") are requested then use a
|
|
11010
11482
|
list like ["child", "parent"].
|
|
11011
11483
|
related_workspace_name (str | None, optional):
|
|
@@ -11015,14 +11487,12 @@ class OTCS:
|
|
|
11015
11487
|
fields (str | list, optional):
|
|
11016
11488
|
Which fields to retrieve. This can have a significant
|
|
11017
11489
|
impact on performance.
|
|
11018
|
-
Possible fields include:
|
|
11490
|
+
Possible fields include (NOTE: "categories" is not supported in this method!!):
|
|
11019
11491
|
- "properties" (can be further restricted by specifying sub-fields,
|
|
11020
11492
|
e.g., "properties{id,name,parent_id,description}")
|
|
11021
|
-
- "
|
|
11022
|
-
- "
|
|
11023
|
-
|
|
11024
|
-
- "permissions" (can be further restricted by specifying ".limit(5)" to
|
|
11025
|
-
retrieve only the first 5 permissions)
|
|
11493
|
+
- "business_properties" (all the information for the business object and the external system)
|
|
11494
|
+
- "workspace_references" (a list with the references to business objects in external systems)
|
|
11495
|
+
- "wksp_info" (currently just the icon information of the workspace)
|
|
11026
11496
|
This parameter can be a string to select one field group or a list of
|
|
11027
11497
|
strings to select multiple field groups.
|
|
11028
11498
|
Defaults to "properties".
|
|
@@ -11032,6 +11502,14 @@ class OTCS:
|
|
|
11032
11502
|
The default is None, in this case the internal OTCS limit seems
|
|
11033
11503
|
to be 500.
|
|
11034
11504
|
This is basically the chunk size for the iterator.
|
|
11505
|
+
limit (int | None = None), optional):
|
|
11506
|
+
The maximum number of workspaces to return in total.
|
|
11507
|
+
If None (default) all workspaces are returned.
|
|
11508
|
+
If a number is provided only up to this number of results is returned.
|
|
11509
|
+
metadata (bool, optional):
|
|
11510
|
+
Whether or not workspace metadata should be returned. These are
|
|
11511
|
+
the system level metadata - not the categories of the workspace!
|
|
11512
|
+
Default is False.
|
|
11035
11513
|
|
|
11036
11514
|
Returns:
|
|
11037
11515
|
iter:
|
|
@@ -11049,6 +11527,7 @@ class OTCS:
|
|
|
11049
11527
|
limit=1,
|
|
11050
11528
|
page=1,
|
|
11051
11529
|
fields=fields,
|
|
11530
|
+
metadata=metadata,
|
|
11052
11531
|
)
|
|
11053
11532
|
if not response or "results" not in response:
|
|
11054
11533
|
# Don't return None! Plain return is what we need for iterators.
|
|
@@ -11057,6 +11536,9 @@ class OTCS:
|
|
|
11057
11536
|
return
|
|
11058
11537
|
|
|
11059
11538
|
number_of_related_workspaces = response["paging"]["total_count"]
|
|
11539
|
+
if limit and number_of_related_workspaces > limit:
|
|
11540
|
+
number_of_related_workspaces = limit
|
|
11541
|
+
|
|
11060
11542
|
if not number_of_related_workspaces:
|
|
11061
11543
|
self.logger.debug(
|
|
11062
11544
|
"Workspace with node ID -> %d does not have related workspaces! Cannot iterate over related workspaces.",
|
|
@@ -11080,9 +11562,10 @@ class OTCS:
|
|
|
11080
11562
|
relationship_type=relationship_type,
|
|
11081
11563
|
related_workspace_name=related_workspace_name,
|
|
11082
11564
|
related_workspace_type_id=related_workspace_type_id,
|
|
11083
|
-
limit=page_size,
|
|
11565
|
+
limit=page_size if limit is None else limit,
|
|
11084
11566
|
page=page,
|
|
11085
11567
|
fields=fields,
|
|
11568
|
+
metadata=metadata,
|
|
11086
11569
|
)
|
|
11087
11570
|
if not response or not response.get("results", None):
|
|
11088
11571
|
self.logger.warning(
|
|
@@ -11314,6 +11797,46 @@ class OTCS:
|
|
|
11314
11797
|
dict | None:
|
|
11315
11798
|
Workspace Role Membership or None if the request fails.
|
|
11316
11799
|
|
|
11800
|
+
Example:
|
|
11801
|
+
{
|
|
11802
|
+
'links': {
|
|
11803
|
+
'data': {
|
|
11804
|
+
'self': {
|
|
11805
|
+
'body': '',
|
|
11806
|
+
'content_type': '',
|
|
11807
|
+
'href': '/api/v2/businessworkspaces/80998/roles/81001/members',
|
|
11808
|
+
'method': 'POST',
|
|
11809
|
+
'name': ''
|
|
11810
|
+
}
|
|
11811
|
+
},
|
|
11812
|
+
'results': {
|
|
11813
|
+
'data': {
|
|
11814
|
+
'properties': {
|
|
11815
|
+
'birth_date': None,
|
|
11816
|
+
'business_email': 'lwhite@terrarium.cloud',
|
|
11817
|
+
'business_fax': None,
|
|
11818
|
+
'business_phone': '+1 (345) 4626-333',
|
|
11819
|
+
'cell_phone': None,
|
|
11820
|
+
'deleted': False,
|
|
11821
|
+
'display_language': None,
|
|
11822
|
+
'display_name': 'Liz White',
|
|
11823
|
+
'first_name': 'Liz',
|
|
11824
|
+
'gender': None,
|
|
11825
|
+
'group_id': 16178,
|
|
11826
|
+
'group_name': 'Executive Leadership Team',
|
|
11827
|
+
'home_address_1': None,
|
|
11828
|
+
'home_address_2': None,
|
|
11829
|
+
'home_fax': None,
|
|
11830
|
+
'home_phone': None,
|
|
11831
|
+
'id': 15520,
|
|
11832
|
+
'initials': 'LW',
|
|
11833
|
+
'last_name': 'White',
|
|
11834
|
+
...
|
|
11835
|
+
}
|
|
11836
|
+
}
|
|
11837
|
+
}
|
|
11838
|
+
}
|
|
11839
|
+
|
|
11317
11840
|
"""
|
|
11318
11841
|
|
|
11319
11842
|
self.logger.debug(
|
|
@@ -12022,7 +12545,7 @@ class OTCS:
|
|
|
12022
12545
|
|
|
12023
12546
|
Args:
|
|
12024
12547
|
parent_id (int):
|
|
12025
|
-
The node the
|
|
12548
|
+
The parent node of the new node to create.
|
|
12026
12549
|
subtype (int, optional):
|
|
12027
12550
|
The subtype of the new node. Default is document.
|
|
12028
12551
|
category_ids (int | list[int], optional):
|
|
@@ -12030,7 +12553,7 @@ class OTCS:
|
|
|
12030
12553
|
|
|
12031
12554
|
Returns:
|
|
12032
12555
|
dict | None:
|
|
12033
|
-
|
|
12556
|
+
Node create form data or None if the request fails.
|
|
12034
12557
|
|
|
12035
12558
|
"""
|
|
12036
12559
|
|
|
@@ -12065,6 +12588,151 @@ class OTCS:
|
|
|
12065
12588
|
|
|
12066
12589
|
# end method definition
|
|
12067
12590
|
|
|
12591
|
+
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="get_node_category_update_form")
|
|
12592
|
+
def get_node_category_form(
|
|
12593
|
+
self,
|
|
12594
|
+
node_id: int,
|
|
12595
|
+
category_id: int | None = None,
|
|
12596
|
+
operation: str = "update",
|
|
12597
|
+
) -> dict | None:
|
|
12598
|
+
"""Get the node category update form.
|
|
12599
|
+
|
|
12600
|
+
Args:
|
|
12601
|
+
node_id (int):
|
|
12602
|
+
The ID of the node to update.
|
|
12603
|
+
category_id (int | None, optional):
|
|
12604
|
+
The ID of the category to update.
|
|
12605
|
+
operation (str, optional):
|
|
12606
|
+
The operation to perform. Default is "update". Other possible value is "create".
|
|
12607
|
+
|
|
12608
|
+
Returns:
|
|
12609
|
+
dict | None:
|
|
12610
|
+
Workspace Category Update Form data or None if the request fails.
|
|
12611
|
+
|
|
12612
|
+
Example:
|
|
12613
|
+
{
|
|
12614
|
+
'forms': [
|
|
12615
|
+
{
|
|
12616
|
+
'data': {
|
|
12617
|
+
'20581_1': {'metadata_token': ''},
|
|
12618
|
+
'20581_10': None,
|
|
12619
|
+
'20581_11': None,
|
|
12620
|
+
'20581_12': None,
|
|
12621
|
+
'20581_13': None,
|
|
12622
|
+
'20581_14': [
|
|
12623
|
+
{
|
|
12624
|
+
'20581_14_x_15': None,
|
|
12625
|
+
'20581_14_x_16': None,
|
|
12626
|
+
'20581_14_x_17': None,
|
|
12627
|
+
'20581_14_x_18': None,
|
|
12628
|
+
'20581_14_x_19': None,
|
|
12629
|
+
'20581_14_x_20': None,
|
|
12630
|
+
'20581_14_x_21': None,
|
|
12631
|
+
'20581_14_x_22': None
|
|
12632
|
+
}
|
|
12633
|
+
],
|
|
12634
|
+
'20581_14_1': None,
|
|
12635
|
+
'20581_2': None,
|
|
12636
|
+
'20581_23': [
|
|
12637
|
+
{
|
|
12638
|
+
'20581_23_x_25': None,
|
|
12639
|
+
'20581_23_x_26': None,
|
|
12640
|
+
'20581_23_x_27': None,
|
|
12641
|
+
'20581_23_x_28': None,
|
|
12642
|
+
'20581_23_x_29': None,
|
|
12643
|
+
'20581_23_x_30': None,
|
|
12644
|
+
'20581_23_x_31': None,
|
|
12645
|
+
'20581_23_x_32': None,
|
|
12646
|
+
'20581_23_x_37': None
|
|
12647
|
+
}
|
|
12648
|
+
],
|
|
12649
|
+
'20581_23_1': None,
|
|
12650
|
+
'20581_3': None,
|
|
12651
|
+
'20581_33': {
|
|
12652
|
+
'20581_33_1_34': None,
|
|
12653
|
+
'20581_33_1_35': None,
|
|
12654
|
+
'20581_33_1_36': None
|
|
12655
|
+
},
|
|
12656
|
+
'20581_4': None,
|
|
12657
|
+
'20581_5': None,
|
|
12658
|
+
'20581_6': None,
|
|
12659
|
+
'20581_7': None,
|
|
12660
|
+
'20581_8': None,
|
|
12661
|
+
'20581_9': None
|
|
12662
|
+
},
|
|
12663
|
+
'options': {
|
|
12664
|
+
'fields': {...},
|
|
12665
|
+
'form': {...}
|
|
12666
|
+
},
|
|
12667
|
+
'schema': {
|
|
12668
|
+
'properties': {
|
|
12669
|
+
'20581_1': {
|
|
12670
|
+
'readonly': True,
|
|
12671
|
+
'required': False, 'type': 'object'},
|
|
12672
|
+
'20581_2': {
|
|
12673
|
+
'maxLength': 20,
|
|
12674
|
+
'multilingual': None,
|
|
12675
|
+
'readonly': False,
|
|
12676
|
+
'required': False,
|
|
12677
|
+
'title': 'Order Number',
|
|
12678
|
+
'type': 'string'
|
|
12679
|
+
},
|
|
12680
|
+
'20581_11': {
|
|
12681
|
+
'maxLength': 25,
|
|
12682
|
+
'multilingual': None,
|
|
12683
|
+
'readonly': False,
|
|
12684
|
+
'required': False,
|
|
12685
|
+
'title': 'Order Type',
|
|
12686
|
+
'type': 'string'
|
|
12687
|
+
},
|
|
12688
|
+
... (more fields) ...
|
|
12689
|
+
},
|
|
12690
|
+
'type': 'object'
|
|
12691
|
+
}
|
|
12692
|
+
}
|
|
12693
|
+
]
|
|
12694
|
+
}
|
|
12695
|
+
|
|
12696
|
+
"""
|
|
12697
|
+
|
|
12698
|
+
request_header = self.request_form_header()
|
|
12699
|
+
|
|
12700
|
+
# If no category ID is provided get the current category IDs of the node and take the first one.
|
|
12701
|
+
# TODO: we need to be more clever here if multiple categories are assigned to a node.
|
|
12702
|
+
if category_id is None:
|
|
12703
|
+
category_ids = self.get_node_category_ids(node_id=node_id)
|
|
12704
|
+
if not category_ids or not isinstance(category_ids, list):
|
|
12705
|
+
self.logger.error("Cannot get category IDs for node with ID -> %s", str(node_id))
|
|
12706
|
+
return None
|
|
12707
|
+
category_id = category_ids[0]
|
|
12708
|
+
|
|
12709
|
+
self.logger.debug(
|
|
12710
|
+
"Get category %s form for node ID -> %s and category ID -> %s",
|
|
12711
|
+
operation,
|
|
12712
|
+
str(node_id),
|
|
12713
|
+
str(category_id),
|
|
12714
|
+
)
|
|
12715
|
+
|
|
12716
|
+
request_url = self.config()["nodesFormUrl"] + "/categories/{}?id={}&category_id={}".format(
|
|
12717
|
+
operation, node_id, category_id
|
|
12718
|
+
)
|
|
12719
|
+
|
|
12720
|
+
response = self.do_request(
|
|
12721
|
+
url=request_url,
|
|
12722
|
+
method="GET",
|
|
12723
|
+
headers=request_header,
|
|
12724
|
+
timeout=None,
|
|
12725
|
+
failure_message="Cannot get category {} form for node ID -> {} and category ID -> {}".format(
|
|
12726
|
+
operation,
|
|
12727
|
+
node_id,
|
|
12728
|
+
category_id,
|
|
12729
|
+
),
|
|
12730
|
+
)
|
|
12731
|
+
|
|
12732
|
+
return response
|
|
12733
|
+
|
|
12734
|
+
# end method definition
|
|
12735
|
+
|
|
12068
12736
|
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="set_system_attributes")
|
|
12069
12737
|
def set_system_attributes(
|
|
12070
12738
|
self,
|
|
@@ -12465,7 +13133,7 @@ class OTCS:
|
|
|
12465
13133
|
request_header = self.request_form_header()
|
|
12466
13134
|
|
|
12467
13135
|
self.logger.debug(
|
|
12468
|
-
"Running
|
|
13136
|
+
"Running web report with nickname -> '%s'; calling -> %s",
|
|
12469
13137
|
nickname,
|
|
12470
13138
|
request_url,
|
|
12471
13139
|
)
|
|
@@ -12503,7 +13171,7 @@ class OTCS:
|
|
|
12503
13171
|
request_header = self.request_form_header()
|
|
12504
13172
|
|
|
12505
13173
|
self.logger.debug(
|
|
12506
|
-
"Install
|
|
13174
|
+
"Install OTCS application -> '%s'; calling -> %s",
|
|
12507
13175
|
application_name,
|
|
12508
13176
|
request_url,
|
|
12509
13177
|
)
|
|
@@ -12514,7 +13182,7 @@ class OTCS:
|
|
|
12514
13182
|
headers=request_header,
|
|
12515
13183
|
data=install_cs_application_post_data,
|
|
12516
13184
|
timeout=None,
|
|
12517
|
-
failure_message="Failed to install
|
|
13185
|
+
failure_message="Failed to install OTCS application -> '{}'".format(
|
|
12518
13186
|
application_name,
|
|
12519
13187
|
),
|
|
12520
13188
|
)
|
|
@@ -12783,6 +13451,64 @@ class OTCS:
|
|
|
12783
13451
|
|
|
12784
13452
|
# end method definition
|
|
12785
13453
|
|
|
13454
|
+
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="check_user_node_permissions")
|
|
13455
|
+
def check_user_node_permissions(self, node_ids: list[int]) -> dict | None:
|
|
13456
|
+
"""Check if the current user has permissions to access a given list of Content Server nodes.
|
|
13457
|
+
|
|
13458
|
+
This is using the AI endpoint has this method is typically used in Aviator use cases.
|
|
13459
|
+
|
|
13460
|
+
Args:
|
|
13461
|
+
node_ids (list[int]):
|
|
13462
|
+
List of node IDs to check.
|
|
13463
|
+
|
|
13464
|
+
Returns:
|
|
13465
|
+
dict | None:
|
|
13466
|
+
REST API response or None in case of an error.
|
|
13467
|
+
|
|
13468
|
+
Example:
|
|
13469
|
+
{
|
|
13470
|
+
'links': {
|
|
13471
|
+
'data': {
|
|
13472
|
+
self': {
|
|
13473
|
+
'body': '',
|
|
13474
|
+
'content_type': '',
|
|
13475
|
+
'href': '/api/v2/ai/nodes/permissions/check',
|
|
13476
|
+
'method': 'POST',
|
|
13477
|
+
'name': ''
|
|
13478
|
+
}
|
|
13479
|
+
}
|
|
13480
|
+
},
|
|
13481
|
+
'results': {
|
|
13482
|
+
'ids': [...]
|
|
13483
|
+
}
|
|
13484
|
+
}
|
|
13485
|
+
|
|
13486
|
+
"""
|
|
13487
|
+
|
|
13488
|
+
request_url = self.config()["aiUrl"] + "/permissions/check"
|
|
13489
|
+
request_header = self.request_form_header()
|
|
13490
|
+
|
|
13491
|
+
permission_post_data = {"ids": node_ids}
|
|
13492
|
+
|
|
13493
|
+
if float(self.get_server_version()) < 25.4:
|
|
13494
|
+
permission_post_data["user_hash"] = self.otcs_ticket_hashed()
|
|
13495
|
+
|
|
13496
|
+
self.logger.debug(
|
|
13497
|
+
"Check if current user has permissions to access nodes -> %s; calling -> %s",
|
|
13498
|
+
node_ids,
|
|
13499
|
+
request_url,
|
|
13500
|
+
)
|
|
13501
|
+
|
|
13502
|
+
return self.do_request(
|
|
13503
|
+
url=request_url,
|
|
13504
|
+
method="POST",
|
|
13505
|
+
headers=request_header,
|
|
13506
|
+
data=permission_post_data,
|
|
13507
|
+
failure_message="Failed to check if current user has permissions to access nodes -> {}".format(node_ids),
|
|
13508
|
+
)
|
|
13509
|
+
|
|
13510
|
+
# end method definition
|
|
13511
|
+
|
|
12786
13512
|
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="get_node_categories")
|
|
12787
13513
|
def get_node_categories(self, node_id: int, metadata: bool = True) -> dict | None:
|
|
12788
13514
|
"""Get categories assigned to a node.
|
|
@@ -12946,7 +13672,7 @@ class OTCS:
|
|
|
12946
13672
|
|
|
12947
13673
|
Returns:
|
|
12948
13674
|
dict | None:
|
|
12949
|
-
REST
|
|
13675
|
+
REST response with category data or None if the call to the REST API fails.
|
|
12950
13676
|
|
|
12951
13677
|
"""
|
|
12952
13678
|
|
|
@@ -13209,7 +13935,127 @@ class OTCS:
|
|
|
13209
13935
|
|
|
13210
13936
|
return cat_id, attribute_definitions
|
|
13211
13937
|
|
|
13212
|
-
return -1, {}
|
|
13938
|
+
return -1, {}
|
|
13939
|
+
|
|
13940
|
+
# end method definition
|
|
13941
|
+
|
|
13942
|
+
def get_category_id_by_name(self, node_id: int, category_name: str) -> int | None:
|
|
13943
|
+
"""Get the category ID by its name.
|
|
13944
|
+
|
|
13945
|
+
Args:
|
|
13946
|
+
node_id (int):
|
|
13947
|
+
The ID of the node to get the categories for.
|
|
13948
|
+
category_name (str):
|
|
13949
|
+
The name of the category to get the ID for.
|
|
13950
|
+
|
|
13951
|
+
Returns:
|
|
13952
|
+
int | None:
|
|
13953
|
+
The category ID or None if the category is not found.
|
|
13954
|
+
|
|
13955
|
+
"""
|
|
13956
|
+
|
|
13957
|
+
response = self.get_node_categories(node_id=node_id)
|
|
13958
|
+
results = response["results"]
|
|
13959
|
+
for result in results:
|
|
13960
|
+
categories = result["metadata"]["categories"]
|
|
13961
|
+
first_key = next(iter(categories))
|
|
13962
|
+
if categories[first_key]["name"] == category_name:
|
|
13963
|
+
return first_key
|
|
13964
|
+
return None
|
|
13965
|
+
|
|
13966
|
+
# end method definition
|
|
13967
|
+
|
|
13968
|
+
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="get_category_as_dictionary")
|
|
13969
|
+
def get_node_category_as_dictionary(
|
|
13970
|
+
self, node_id: int, category_id: int | None = None, category_name: str | None = None
|
|
13971
|
+
) -> dict | None:
|
|
13972
|
+
"""Get a specific category assigned to a node in a streamlined Python dictionary form.
|
|
13973
|
+
|
|
13974
|
+
* The whole category data of a node is embedded into a python dict.
|
|
13975
|
+
* Single-value / scalar attributes are key / value pairs in that dict.
|
|
13976
|
+
* Multi-value attributes become key / value pairs with value being a list of strings or integers.
|
|
13977
|
+
* Single-line sets become key /value pairs with value being a sub-dict.
|
|
13978
|
+
* Attribute in single-line sets become key / value pairs in the sub-dict.
|
|
13979
|
+
* Multi-line sets become key / value pairs with value being a list of dicts.
|
|
13980
|
+
* Single-value attributes in multi-line sets become key / value pairs inside the dict at the row position in the list.
|
|
13981
|
+
* Multi-value attributes in multi-line sets become key / value pairs inside the dict at the row position in the list
|
|
13982
|
+
with value being a list of strings or integers.
|
|
13983
|
+
|
|
13984
|
+
See also extract_category_data() for an alternative implementation.
|
|
13985
|
+
|
|
13986
|
+
Args:
|
|
13987
|
+
node_id (int):
|
|
13988
|
+
The ID of the node to get the categories for.
|
|
13989
|
+
category_id (int | None, optional):
|
|
13990
|
+
The node ID of the category definition (in category volume). If not provided,
|
|
13991
|
+
the category ID is determined by its name.
|
|
13992
|
+
category_name (str | None, optional):
|
|
13993
|
+
The name of the category to get the ID for.
|
|
13994
|
+
If category_id is not provided, the category ID is determined by its name.
|
|
13995
|
+
|
|
13996
|
+
Returns:
|
|
13997
|
+
dict | None:
|
|
13998
|
+
REST response with category data or None if the call to the REST API fails.
|
|
13999
|
+
|
|
14000
|
+
"""
|
|
14001
|
+
|
|
14002
|
+
if not category_id and not category_name:
|
|
14003
|
+
self.logger.error("Either category ID or category name must be provided!")
|
|
14004
|
+
return None
|
|
14005
|
+
|
|
14006
|
+
if not category_id:
|
|
14007
|
+
category_id = self.get_category_id_by_name(node_id=node_id, category_name=category_name)
|
|
14008
|
+
|
|
14009
|
+
response = self.get_node_category(node_id=node_id, category_id=category_id)
|
|
14010
|
+
|
|
14011
|
+
data = response["results"]["data"]["categories"]
|
|
14012
|
+
metadata = response["results"]["metadata"]["categories"]
|
|
14013
|
+
category_key = next(iter(metadata))
|
|
14014
|
+
_ = metadata.pop(category_key)
|
|
14015
|
+
|
|
14016
|
+
# Initialize the result dict:
|
|
14017
|
+
result = {}
|
|
14018
|
+
|
|
14019
|
+
for key, attribute in metadata.items():
|
|
14020
|
+
is_set = attribute["persona"] == "set"
|
|
14021
|
+
is_multi_value = attribute["multi_value"]
|
|
14022
|
+
attr_name = attribute["name"]
|
|
14023
|
+
attr_key = attribute["key"]
|
|
14024
|
+
|
|
14025
|
+
if is_set:
|
|
14026
|
+
set_name = attr_name
|
|
14027
|
+
set_multi_value = is_multi_value
|
|
14028
|
+
|
|
14029
|
+
if not is_set and "x" not in attr_key:
|
|
14030
|
+
result[attr_name] = data[key]
|
|
14031
|
+
set_name = None
|
|
14032
|
+
elif is_set:
|
|
14033
|
+
# The current attribute is the set itself:
|
|
14034
|
+
if not is_multi_value:
|
|
14035
|
+
result[attr_name] = {}
|
|
14036
|
+
else:
|
|
14037
|
+
result[attr_name] = []
|
|
14038
|
+
set_name = attr_name
|
|
14039
|
+
elif not is_set and "x" in attr_key:
|
|
14040
|
+
# We re inside a set and process the set attributes:
|
|
14041
|
+
if not set_multi_value:
|
|
14042
|
+
# A single row set:
|
|
14043
|
+
attr_key = attr_key.replace("_x_", "_1_")
|
|
14044
|
+
result[set_name][attr_name] = data[attr_key]
|
|
14045
|
+
else:
|
|
14046
|
+
# Collect all the row data:
|
|
14047
|
+
for index in range(1, 50):
|
|
14048
|
+
attr_key_index = attr_key.replace("_x_", "_" + str(index) + "_")
|
|
14049
|
+
# Do we have data for this row?
|
|
14050
|
+
if attr_key_index in data:
|
|
14051
|
+
if index > len(result[set_name]):
|
|
14052
|
+
result[set_name].append({attr_name: data[attr_key_index]})
|
|
14053
|
+
else:
|
|
14054
|
+
result[set_name][index - 1][attr_name] = data[attr_key_index]
|
|
14055
|
+
else:
|
|
14056
|
+
# No more rows
|
|
14057
|
+
break
|
|
14058
|
+
return result
|
|
13213
14059
|
|
|
13214
14060
|
# end method definition
|
|
13215
14061
|
|
|
@@ -13223,6 +14069,7 @@ class OTCS:
|
|
|
13223
14069
|
apply_action: str = "add_upgrade",
|
|
13224
14070
|
add_version: bool = False,
|
|
13225
14071
|
clear_existing_categories: bool = False,
|
|
14072
|
+
attribute_values: dict | None = None,
|
|
13226
14073
|
) -> bool:
|
|
13227
14074
|
"""Assign a category to a Content Server node.
|
|
13228
14075
|
|
|
@@ -13230,6 +14077,7 @@ class OTCS:
|
|
|
13230
14077
|
(if node_id is a container / folder / workspace).
|
|
13231
14078
|
If the category is already assigned to the node this method will
|
|
13232
14079
|
throw an error.
|
|
14080
|
+
Optionally set category attributes values.
|
|
13233
14081
|
|
|
13234
14082
|
Args:
|
|
13235
14083
|
node_id (int):
|
|
@@ -13250,6 +14098,9 @@ class OTCS:
|
|
|
13250
14098
|
True, if a document version should be added for the category change (default = False).
|
|
13251
14099
|
clear_existing_categories (bool, optional):
|
|
13252
14100
|
Defines, whether or not existing (other) categories should be removed (default = False).
|
|
14101
|
+
attribute_values (dict, optional):
|
|
14102
|
+
Dictionary containing "attribute_id":"value" pairs, to be populated during the category assignment.
|
|
14103
|
+
(In case of the category attributes being set as "Required" in xECM, providing corresponding values for those attributes will resolve inability to assign the category).
|
|
13253
14104
|
|
|
13254
14105
|
Returns:
|
|
13255
14106
|
bool:
|
|
@@ -13275,6 +14126,9 @@ class OTCS:
|
|
|
13275
14126
|
"category_id": category_id,
|
|
13276
14127
|
}
|
|
13277
14128
|
|
|
14129
|
+
if attribute_values is not None:
|
|
14130
|
+
category_post_data.update(attribute_values)
|
|
14131
|
+
|
|
13278
14132
|
self.logger.debug(
|
|
13279
14133
|
"Assign category with ID -> %d to item with ID -> %d; calling -> %s",
|
|
13280
14134
|
category_id,
|
|
@@ -13735,6 +14589,8 @@ class OTCS:
|
|
|
13735
14589
|
* Multi-value attributes in multi-line sets become key / value pairs inside the dict at the row position in the list
|
|
13736
14590
|
with value being a list of strings or integers.
|
|
13737
14591
|
|
|
14592
|
+
See also get_node_category_as_dictionary() for an alternative implementation.
|
|
14593
|
+
|
|
13738
14594
|
Args:
|
|
13739
14595
|
node (dict):
|
|
13740
14596
|
The typical node response of a node get REST API call that include the "categories" fields.
|
|
@@ -13806,14 +14662,37 @@ class OTCS:
|
|
|
13806
14662
|
|
|
13807
14663
|
# Start of main method body:
|
|
13808
14664
|
|
|
13809
|
-
if not node
|
|
14665
|
+
if not node:
|
|
14666
|
+
self.logger.error("Cannot extract category data. No node data provided!")
|
|
13810
14667
|
return None
|
|
13811
14668
|
|
|
14669
|
+
if "results" not in node:
|
|
14670
|
+
# Support also iterators that have resolved the "results" already.
|
|
14671
|
+
# In this case we wrap it in a "rsults" dict to make it look like
|
|
14672
|
+
# a full response:
|
|
14673
|
+
if "data" in node:
|
|
14674
|
+
node = {"results": node}
|
|
14675
|
+
else:
|
|
14676
|
+
return None
|
|
14677
|
+
|
|
14678
|
+
# Some OTCS REST APIs may return a list of nodes in "results".
|
|
14679
|
+
# We only support processing a single node here:
|
|
14680
|
+
if isinstance(node["results"], list):
|
|
14681
|
+
if len(node["results"]) > 1:
|
|
14682
|
+
self.logger.warning("Response includes a node list. Extracting category data for the first node!")
|
|
14683
|
+
node["results"] = node["results"][0]
|
|
14684
|
+
|
|
13812
14685
|
if "metadata" not in node["results"]:
|
|
13813
|
-
self.logger.error("Cannot
|
|
14686
|
+
self.logger.error("Cannot extract category data. Method was called without the '&metadata' parameter!")
|
|
13814
14687
|
return None
|
|
13815
14688
|
|
|
13816
|
-
|
|
14689
|
+
metadata = node["results"]["metadata"]
|
|
14690
|
+
if "categories" not in metadata:
|
|
14691
|
+
self.logger.error(
|
|
14692
|
+
"Cannot extract category data. No category data found in node response! Use 'categories' value for 'fields' parameter in the node call!"
|
|
14693
|
+
)
|
|
14694
|
+
return None
|
|
14695
|
+
category_schemas = metadata["categories"]
|
|
13817
14696
|
|
|
13818
14697
|
result_dict = {}
|
|
13819
14698
|
current_dict = result_dict
|
|
@@ -13821,6 +14700,15 @@ class OTCS:
|
|
|
13821
14700
|
category_lookup = {}
|
|
13822
14701
|
attribute_lookup = {}
|
|
13823
14702
|
|
|
14703
|
+
# Some REST API return categories in different format. We adjust
|
|
14704
|
+
# it on the fly here:
|
|
14705
|
+
if isinstance(category_schemas, list):
|
|
14706
|
+
new_schema = {}
|
|
14707
|
+
for category_schema in category_schemas:
|
|
14708
|
+
first_key = next(iter(category_schema))
|
|
14709
|
+
new_schema[first_key] = category_schema
|
|
14710
|
+
category_schemas = new_schema
|
|
14711
|
+
|
|
13824
14712
|
try:
|
|
13825
14713
|
for category_key, category_schema in category_schemas.items():
|
|
13826
14714
|
for attribute_key, attribute_schema in category_schema.items():
|
|
@@ -13888,9 +14776,17 @@ class OTCS:
|
|
|
13888
14776
|
self.logger.error("Type -> '%s' not handled yet!", attribute_type)
|
|
13889
14777
|
except Exception as e:
|
|
13890
14778
|
self.logger.error("Something went wrong with getting the data schema! Error -> %s", str(e))
|
|
14779
|
+
return None
|
|
13891
14780
|
|
|
13892
14781
|
category_datas = node["results"]["data"]["categories"]
|
|
13893
14782
|
|
|
14783
|
+
if isinstance(category_datas, list):
|
|
14784
|
+
new_data = {}
|
|
14785
|
+
for category_data in category_datas:
|
|
14786
|
+
first_key = next(iter(category_data))
|
|
14787
|
+
new_data[first_key] = category_data
|
|
14788
|
+
category_datas = new_data
|
|
14789
|
+
|
|
13894
14790
|
try:
|
|
13895
14791
|
for category_data in category_datas.values():
|
|
13896
14792
|
for attribute_key, value in category_data.items():
|
|
@@ -13911,7 +14807,7 @@ class OTCS:
|
|
|
13911
14807
|
current_dict = set_data
|
|
13912
14808
|
elif isinstance(set_data, list):
|
|
13913
14809
|
row_number = get_row_number(attribute_key=attribute_key)
|
|
13914
|
-
self.logger.debug("
|
|
14810
|
+
self.logger.debug("Target dict is row %d of multi-line set attribute.", row_number)
|
|
13915
14811
|
if row_number > len(set_data):
|
|
13916
14812
|
self.logger.debug("Add rows up to %d of multi-line set attribute...", row_number)
|
|
13917
14813
|
for _ in range(row_number - len(set_data)):
|
|
@@ -13927,6 +14823,7 @@ class OTCS:
|
|
|
13927
14823
|
current_dict[attribute_name] = value
|
|
13928
14824
|
except Exception as e:
|
|
13929
14825
|
self.logger.error("Something went wrong while filling the data! Error -> %s", str(e))
|
|
14826
|
+
return None
|
|
13930
14827
|
|
|
13931
14828
|
return result_dict
|
|
13932
14829
|
|
|
@@ -14311,7 +15208,7 @@ class OTCS:
|
|
|
14311
15208
|
|
|
14312
15209
|
Args:
|
|
14313
15210
|
node_id (int):
|
|
14314
|
-
The node ID of the
|
|
15211
|
+
The node ID of the Business Workspace template.
|
|
14315
15212
|
|
|
14316
15213
|
Returns:
|
|
14317
15214
|
dict | None:
|
|
@@ -16468,6 +17365,7 @@ class OTCS:
|
|
|
16468
17365
|
|
|
16469
17366
|
"""
|
|
16470
17367
|
|
|
17368
|
+
# If no sub-process ID is given, use the process ID:
|
|
16471
17369
|
if subprocess_id is None:
|
|
16472
17370
|
subprocess_id = process_id
|
|
16473
17371
|
|
|
@@ -16679,7 +17577,7 @@ class OTCS:
|
|
|
16679
17577
|
"enabled": status,
|
|
16680
17578
|
}
|
|
16681
17579
|
|
|
16682
|
-
request_url = self.config()["
|
|
17580
|
+
request_url = self.config()["aiNodesUrl"] + "/{}".format(workspace_id)
|
|
16683
17581
|
request_header = self.request_form_header()
|
|
16684
17582
|
|
|
16685
17583
|
if status is True:
|
|
@@ -16708,6 +17606,181 @@ class OTCS:
|
|
|
16708
17606
|
|
|
16709
17607
|
# end method definition
|
|
16710
17608
|
|
|
17609
|
+
def aviator_chat(
|
|
17610
|
+
self, context: str | None, messages: list[dict], where: list[dict] | None = None, inline_citation: bool = True
|
|
17611
|
+
) -> dict | None:
|
|
17612
|
+
"""Process a chat interaction with Content Aviator.
|
|
17613
|
+
|
|
17614
|
+
Args:
|
|
17615
|
+
context (str | None):
|
|
17616
|
+
Context for the current conversation. This includes the text chunks
|
|
17617
|
+
provided by the RAG pipeline.
|
|
17618
|
+
messages (list[dict]):
|
|
17619
|
+
List of messages from conversation history.
|
|
17620
|
+
Format example:
|
|
17621
|
+
[
|
|
17622
|
+
{
|
|
17623
|
+
"author": "user", "content": "Summarize this workspace, please."
|
|
17624
|
+
},
|
|
17625
|
+
{
|
|
17626
|
+
"author": "ai", "content": "..."
|
|
17627
|
+
}
|
|
17628
|
+
]
|
|
17629
|
+
where (list):
|
|
17630
|
+
Metadata name/value pairs for the query.
|
|
17631
|
+
Could be used to specify workspaces, documents, or other criteria in the future.
|
|
17632
|
+
Values need to match those passed as metadata to the embeddings API.
|
|
17633
|
+
Format example:
|
|
17634
|
+
[
|
|
17635
|
+
{"workspaceID":"38673"},
|
|
17636
|
+
{"documentID":"38458"},
|
|
17637
|
+
]
|
|
17638
|
+
inline_citation (bool, optional):
|
|
17639
|
+
Whether or not inline citations should be used in the response. Default is True.
|
|
17640
|
+
|
|
17641
|
+
Returns:
|
|
17642
|
+
dict:
|
|
17643
|
+
Conversation status
|
|
17644
|
+
|
|
17645
|
+
Example:
|
|
17646
|
+
{
|
|
17647
|
+
'result': 'I am unable to provide the three main regulations for fuel, as the documents contain various articles and specifications related to fuel, but do not explicitly identify which three are the "main" ones.',
|
|
17648
|
+
'references': [
|
|
17649
|
+
{
|
|
17650
|
+
'chunks': [
|
|
17651
|
+
{
|
|
17652
|
+
'citation': None,
|
|
17653
|
+
'content': ['16. 1 Basic principles 16.'],
|
|
17654
|
+
'distance': 0.262610273676197,
|
|
17655
|
+
'source': 'Similarity'
|
|
17656
|
+
}
|
|
17657
|
+
],
|
|
17658
|
+
'distance': 0.262610273676197,
|
|
17659
|
+
'metadata': {
|
|
17660
|
+
'content': {
|
|
17661
|
+
'chunks': ['16. 1 Basic principles 16.'],
|
|
17662
|
+
'source': 'Similarity'
|
|
17663
|
+
},
|
|
17664
|
+
'documentID': '39004',
|
|
17665
|
+
'workspaceID': '38673'
|
|
17666
|
+
}
|
|
17667
|
+
},
|
|
17668
|
+
{
|
|
17669
|
+
'chunks': [
|
|
17670
|
+
{
|
|
17671
|
+
'citation': None,
|
|
17672
|
+
'content': ['16. 1.'],
|
|
17673
|
+
'distance': 0.284182507756566,
|
|
17674
|
+
'source': 'Similarity'
|
|
17675
|
+
}
|
|
17676
|
+
],
|
|
17677
|
+
'distance': 0.284182507756566,
|
|
17678
|
+
'metadata': {
|
|
17679
|
+
'content': {
|
|
17680
|
+
'chunks': ['16. 1.'],
|
|
17681
|
+
'source': 'Similarity'
|
|
17682
|
+
},
|
|
17683
|
+
'documentID': '38123',
|
|
17684
|
+
'workspaceID': '38673'
|
|
17685
|
+
}
|
|
17686
|
+
}
|
|
17687
|
+
],
|
|
17688
|
+
'context': 'Tool "get_context" called with arguments {"query":"Tell me about the calibration equipment"} and returned:',
|
|
17689
|
+
'queryMetadata': {
|
|
17690
|
+
'originalQuery': 'Tell me about the calibration equipment',
|
|
17691
|
+
'usedQuery': 'Tell me about the calibration equipment'
|
|
17692
|
+
}
|
|
17693
|
+
}
|
|
17694
|
+
|
|
17695
|
+
|
|
17696
|
+
|
|
17697
|
+
"""
|
|
17698
|
+
|
|
17699
|
+
request_url = self.config()["aiChatUrl"]
|
|
17700
|
+
request_header = self.request_form_header()
|
|
17701
|
+
|
|
17702
|
+
chat_data = {}
|
|
17703
|
+
if where:
|
|
17704
|
+
chat_data["where"] = where
|
|
17705
|
+
|
|
17706
|
+
chat_data["context"] = context
|
|
17707
|
+
chat_data["messages"] = messages
|
|
17708
|
+
# "synonyms": self.config()["synonyms"],
|
|
17709
|
+
chat_data["inlineCitation"] = inline_citation
|
|
17710
|
+
|
|
17711
|
+
return self.do_request(
|
|
17712
|
+
url=request_url,
|
|
17713
|
+
method="POST",
|
|
17714
|
+
headers=request_header,
|
|
17715
|
+
# data=chat_data,
|
|
17716
|
+
data={"body": json.dumps(chat_data)},
|
|
17717
|
+
timeout=None,
|
|
17718
|
+
failure_message="Failed to chat with Content Aviator",
|
|
17719
|
+
)
|
|
17720
|
+
|
|
17721
|
+
# end method definition
|
|
17722
|
+
|
|
17723
|
+
def aviator_context(
|
|
17724
|
+
self, query: str, threshold: float = 0.5, limit: int = 10, data: list | None = None
|
|
17725
|
+
) -> dict | None:
|
|
17726
|
+
"""Get context based on the query text from Aviator's vector database.
|
|
17727
|
+
|
|
17728
|
+
Results are text-chunks and they will be permission-checked for the authenticated user.
|
|
17729
|
+
|
|
17730
|
+
Args:
|
|
17731
|
+
query (str):
|
|
17732
|
+
The query text to search for similar text chunks.
|
|
17733
|
+
threshold (float, optional):
|
|
17734
|
+
Similarity threshold between 0 and 1. Default is 0.5.
|
|
17735
|
+
limit (int, optional):
|
|
17736
|
+
Maximum number of results to return. Default is 10.
|
|
17737
|
+
data (list | None, optional):
|
|
17738
|
+
Additional data to pass to the embeddings API. Defaults to None.
|
|
17739
|
+
This can include metadata for filtering the results.
|
|
17740
|
+
|
|
17741
|
+
Returns:
|
|
17742
|
+
dict | None:
|
|
17743
|
+
The response from the embeddings API or None if the request fails.
|
|
17744
|
+
|
|
17745
|
+
"""
|
|
17746
|
+
|
|
17747
|
+
request_url = self.config()["aiContextUrl"]
|
|
17748
|
+
request_header = self.request_form_header()
|
|
17749
|
+
|
|
17750
|
+
if not query:
|
|
17751
|
+
self.logger.error("Query text is required for getting context from Content Aviator!")
|
|
17752
|
+
return None
|
|
17753
|
+
|
|
17754
|
+
context_post_body = {
|
|
17755
|
+
"query": query,
|
|
17756
|
+
"threshold": threshold,
|
|
17757
|
+
"limit": limit,
|
|
17758
|
+
}
|
|
17759
|
+
if data:
|
|
17760
|
+
context_post_body["data"] = data
|
|
17761
|
+
else:
|
|
17762
|
+
context_post_body["data"] = []
|
|
17763
|
+
|
|
17764
|
+
self.logger.debug(
|
|
17765
|
+
"Get context from Content Aviator for query -> '%s' (threshold: %f, limit: %d); calling -> %s",
|
|
17766
|
+
query,
|
|
17767
|
+
threshold,
|
|
17768
|
+
limit,
|
|
17769
|
+
request_url,
|
|
17770
|
+
)
|
|
17771
|
+
|
|
17772
|
+
return self.do_request(
|
|
17773
|
+
url=request_url,
|
|
17774
|
+
method="POST",
|
|
17775
|
+
headers=request_header,
|
|
17776
|
+
# data={"body": json.dumps(context_post_body)},
|
|
17777
|
+
data=context_post_body,
|
|
17778
|
+
timeout=None,
|
|
17779
|
+
failure_message="Failed to retrieve context from Content Aviator",
|
|
17780
|
+
)
|
|
17781
|
+
|
|
17782
|
+
# end method definition
|
|
17783
|
+
|
|
16711
17784
|
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="traverse_node")
|
|
16712
17785
|
def traverse_node(
|
|
16713
17786
|
self,
|
|
@@ -16788,7 +17861,8 @@ class OTCS:
|
|
|
16788
17861
|
for subnode in subnodes:
|
|
16789
17862
|
subnode_id = self.get_result_value(response=subnode, key="id")
|
|
16790
17863
|
subnode_name = self.get_result_value(response=subnode, key="name")
|
|
16791
|
-
self.
|
|
17864
|
+
subnode_type_name = self.get_result_value(response=subnode, key="type_name")
|
|
17865
|
+
self.logger.info("Traversing %s node -> '%s' (%s)", subnode_type_name, subnode_name, subnode_id)
|
|
16792
17866
|
# Recursive call for current subnode:
|
|
16793
17867
|
result = self.traverse_node(
|
|
16794
17868
|
node=subnode,
|
|
@@ -16866,7 +17940,13 @@ class OTCS:
|
|
|
16866
17940
|
task_queue.put((subnode, 0, traversal_data))
|
|
16867
17941
|
|
|
16868
17942
|
def traverse_node_worker() -> None:
|
|
16869
|
-
"""Work on queue.
|
|
17943
|
+
"""Work on a shared queue.
|
|
17944
|
+
|
|
17945
|
+
Loops over these steps:
|
|
17946
|
+
1. Get node from queue
|
|
17947
|
+
2. Execute all executables for that node
|
|
17948
|
+
3. If node is a container and executables indicate to traverse,
|
|
17949
|
+
then enqueue all subnodes
|
|
16870
17950
|
|
|
16871
17951
|
Returns:
|
|
16872
17952
|
None
|
|
@@ -16903,6 +17983,17 @@ class OTCS:
|
|
|
16903
17983
|
|
|
16904
17984
|
# Run all executables
|
|
16905
17985
|
for executable in executables or []:
|
|
17986
|
+
# The executables are functions or method from outside this class.
|
|
17987
|
+
# They need to return a tuple of two boolean values:
|
|
17988
|
+
# (result_success, result_traverse)
|
|
17989
|
+
# result_success indicates if the executable was successful (True)
|
|
17990
|
+
# or not (False). If False, the execution of the executables list
|
|
17991
|
+
# is stopped.
|
|
17992
|
+
# result_traverse indicates if the traversal should continue
|
|
17993
|
+
# into subnodes (True) or not (False).
|
|
17994
|
+
# If at least one executable returns result_traverse = True,
|
|
17995
|
+
# then the traversal into subnodes will be done (if the node is a container).
|
|
17996
|
+
# As this code is from outside this class, we better catch exceptions:
|
|
16906
17997
|
try:
|
|
16907
17998
|
result_success, result_traverse = executable(
|
|
16908
17999
|
node=node,
|
|
@@ -17877,7 +18968,8 @@ class OTCS:
|
|
|
17877
18968
|
Defaults to None = filter not active.
|
|
17878
18969
|
filter_subtypes (list | None, optional):
|
|
17879
18970
|
Additive filter criterium for item type.
|
|
17880
|
-
Defaults to None = filter not active.
|
|
18971
|
+
Defaults to None = filter not active. filter_subtypes = [] is different from None!
|
|
18972
|
+
If an empty list is provided, the filter is effectively always True.
|
|
17881
18973
|
filter_category (str | None, optional):
|
|
17882
18974
|
Additive filter criterium for existence of a category on the node.
|
|
17883
18975
|
The value of filter_category is the name of the category
|
|
@@ -17910,7 +19002,7 @@ class OTCS:
|
|
|
17910
19002
|
self.logger.error("Illegal node - cannot apply filter!")
|
|
17911
19003
|
return False
|
|
17912
19004
|
|
|
17913
|
-
if filter_subtypes and node["type"] not in filter_subtypes:
|
|
19005
|
+
if filter_subtypes is not None and node["type"] not in filter_subtypes:
|
|
17914
19006
|
self.logger.debug(
|
|
17915
19007
|
"Node type -> '%s' is not in filter node types -> %s. Node -> '%s' failed filter test.",
|
|
17916
19008
|
node["type"],
|
|
@@ -18623,6 +19715,8 @@ class OTCS:
|
|
|
18623
19715
|
def load_items(
|
|
18624
19716
|
self,
|
|
18625
19717
|
node_id: int,
|
|
19718
|
+
workspaces: bool = True,
|
|
19719
|
+
items: bool = True,
|
|
18626
19720
|
filter_workspace_depth: int | None = None,
|
|
18627
19721
|
filter_workspace_subtypes: list | None = None,
|
|
18628
19722
|
filter_workspace_category: str | None = None,
|
|
@@ -18647,6 +19741,12 @@ class OTCS:
|
|
|
18647
19741
|
Args:
|
|
18648
19742
|
node_id (int):
|
|
18649
19743
|
The root Node ID the traversal should start at.
|
|
19744
|
+
workspaces (bool, optional):
|
|
19745
|
+
If True, workspaces are included in the data frame.
|
|
19746
|
+
Defaults to True.
|
|
19747
|
+
items (bool, optional):
|
|
19748
|
+
If True, document items are included in the data frame.
|
|
19749
|
+
Defaults to True.
|
|
18650
19750
|
filter_workspace_depth (int | None, optional):
|
|
18651
19751
|
Additive filter criterium for workspace path depth.
|
|
18652
19752
|
Defaults to None = filter not active.
|
|
@@ -18695,9 +19795,30 @@ class OTCS:
|
|
|
18695
19795
|
dict:
|
|
18696
19796
|
Stats with processed and traversed counters.
|
|
18697
19797
|
|
|
19798
|
+
Side Effects:
|
|
19799
|
+
The resulting data frame is stored in self._data. It will have the following columns:
|
|
19800
|
+
- type which is either "item" or "workspace"
|
|
19801
|
+
- workspace_type
|
|
19802
|
+
- workspace_id
|
|
19803
|
+
- workspace_name
|
|
19804
|
+
- workspace_description
|
|
19805
|
+
- workspace_outer_path
|
|
19806
|
+
- workspace_<cat_id>_<attr_id> for each workspace attribute if workspace_metadata is True
|
|
19807
|
+
- item_id
|
|
19808
|
+
- item_type
|
|
19809
|
+
- item_name
|
|
19810
|
+
- item_description
|
|
19811
|
+
- item_path
|
|
19812
|
+
- item_download_name
|
|
19813
|
+
- item_mime_type
|
|
19814
|
+
- item_url
|
|
19815
|
+
- item_<cat_id>_<attr_id> for each item attribute if item_metadata is True
|
|
19816
|
+
- item_cat_<cat_id>_<attr_id> for each item attribute if item_metadata is True and self._use_numeric_category_identifier is True
|
|
19817
|
+
- item_cat_<cat_name>_<attr_name> for each item attribute if item_metadata is True and self._use_numeric_category_identifier is False
|
|
19818
|
+
|
|
18698
19819
|
"""
|
|
18699
19820
|
|
|
18700
|
-
# Initiaze download threads for
|
|
19821
|
+
# Initiaze download threads for document items:
|
|
18701
19822
|
download_threads = []
|
|
18702
19823
|
|
|
18703
19824
|
def check_node_exclusions(node: dict, **kwargs: dict) -> tuple[bool, bool]:
|
|
@@ -18718,15 +19839,14 @@ class OTCS:
|
|
|
18718
19839
|
|
|
18719
19840
|
"""
|
|
18720
19841
|
|
|
18721
|
-
|
|
18722
|
-
|
|
18723
|
-
|
|
18724
|
-
return (False, False)
|
|
19842
|
+
# Get the list of node IDs to exclude from the keyword arguments.
|
|
19843
|
+
# If not provided, use an empty list as default which means no exclusions.
|
|
19844
|
+
exclude_node_ids = kwargs.get("exclude_node_ids") or []
|
|
18725
19845
|
|
|
18726
19846
|
node_id = self.get_result_value(response=node, key="id")
|
|
18727
19847
|
node_name = self.get_result_value(response=node, key="name")
|
|
18728
19848
|
|
|
18729
|
-
if node_id and
|
|
19849
|
+
if node_id and (node_id in exclude_node_ids):
|
|
18730
19850
|
self.logger.info(
|
|
18731
19851
|
"Node -> '%s' (%s) is in exclusion list. Skip traversal of this node.",
|
|
18732
19852
|
node_name,
|
|
@@ -18753,13 +19873,36 @@ class OTCS:
|
|
|
18753
19873
|
|
|
18754
19874
|
"""
|
|
18755
19875
|
|
|
19876
|
+
# This should actually not happen as the caller should
|
|
19877
|
+
# check if workspaces are requested before calling this function.
|
|
19878
|
+
if not workspaces:
|
|
19879
|
+
# Success = False, Traverse = True
|
|
19880
|
+
return (False, True)
|
|
19881
|
+
|
|
18756
19882
|
traversal_data = kwargs.get("traversal_data")
|
|
18757
19883
|
filter_workspace_data = kwargs.get("filter_workspace_data")
|
|
18758
19884
|
control_flags = kwargs.get("control_flags")
|
|
18759
19885
|
|
|
18760
|
-
if not traversal_data
|
|
18761
|
-
self.logger.error(
|
|
18762
|
-
|
|
19886
|
+
if not traversal_data:
|
|
19887
|
+
self.logger.error(
|
|
19888
|
+
"Missing keyword argument 'traversal_data' for executable 'check_node_workspace' in node traversal!"
|
|
19889
|
+
)
|
|
19890
|
+
# Success = False, Traverse = False
|
|
19891
|
+
return (False, False)
|
|
19892
|
+
|
|
19893
|
+
if not filter_workspace_data:
|
|
19894
|
+
self.logger.error(
|
|
19895
|
+
"Missing keyword argument 'filter_workspace_data' for executable 'check_node_workspace' in node traversal!"
|
|
19896
|
+
)
|
|
19897
|
+
# Success = False, Traverse = False
|
|
19898
|
+
return (False, False)
|
|
19899
|
+
|
|
19900
|
+
if not control_flags:
|
|
19901
|
+
self.logger.error(
|
|
19902
|
+
"Missing keyword argument 'control_flags' for executable 'check_node_workspace' in node traversal!"
|
|
19903
|
+
)
|
|
19904
|
+
# Success = False, Traverse = False
|
|
19905
|
+
return (False, False)
|
|
18763
19906
|
|
|
18764
19907
|
node_id = self.get_result_value(response=node, key="id")
|
|
18765
19908
|
node_name = self.get_result_value(response=node, key="name")
|
|
@@ -18767,10 +19910,10 @@ class OTCS:
|
|
|
18767
19910
|
node_type = self.get_result_value(response=node, key="type")
|
|
18768
19911
|
|
|
18769
19912
|
#
|
|
18770
|
-
# 1. Check if the traversal is already inside a
|
|
18771
|
-
# the workspace processing
|
|
19913
|
+
# 1. Check if the traversal is already inside a workspace. Then we can skip
|
|
19914
|
+
# the workspace processing as we currently don't support sub-workspaces.
|
|
18772
19915
|
#
|
|
18773
|
-
workspace_id = traversal_data
|
|
19916
|
+
workspace_id = traversal_data.get("workspace_id")
|
|
18774
19917
|
if workspace_id:
|
|
18775
19918
|
self.logger.debug(
|
|
18776
19919
|
"Found folder or workspace -> '%s' (%s) inside workspace with ID -> %d. So this container cannot be a workspace.",
|
|
@@ -18801,7 +19944,7 @@ class OTCS:
|
|
|
18801
19944
|
categories = None
|
|
18802
19945
|
|
|
18803
19946
|
#
|
|
18804
|
-
# 3. Apply the defined filters to the current node to see
|
|
19947
|
+
# 3. Apply the defined workspace filters to the current node to see
|
|
18805
19948
|
# if we want to 'interpret' it as a workspace
|
|
18806
19949
|
#
|
|
18807
19950
|
# See if it is a node that we want to interpret as a workspace.
|
|
@@ -18818,6 +19961,13 @@ class OTCS:
|
|
|
18818
19961
|
filter_category=filter_workspace_data["filter_workspace_category"],
|
|
18819
19962
|
filter_attributes=filter_workspace_data["filter_workspace_attributes"],
|
|
18820
19963
|
):
|
|
19964
|
+
self.logger.debug(
|
|
19965
|
+
"Node -> '%s' (%s) did not match workspace filter -> %s",
|
|
19966
|
+
node_name,
|
|
19967
|
+
node_id,
|
|
19968
|
+
str(filter_workspace_data),
|
|
19969
|
+
)
|
|
19970
|
+
|
|
18821
19971
|
# Success = False, Traverse = True
|
|
18822
19972
|
return (False, True)
|
|
18823
19973
|
|
|
@@ -18831,7 +19981,7 @@ class OTCS:
|
|
|
18831
19981
|
#
|
|
18832
19982
|
# 4. Create the data frame row from the node / traversal data:
|
|
18833
19983
|
#
|
|
18834
|
-
row = {}
|
|
19984
|
+
row = {"type": "workspace"}
|
|
18835
19985
|
row["workspace_type"] = node_type
|
|
18836
19986
|
row["workspace_id"] = node_id
|
|
18837
19987
|
row["workspace_name"] = node_name
|
|
@@ -18855,7 +20005,7 @@ class OTCS:
|
|
|
18855
20005
|
traversal_data["workspace_description"] = node_description
|
|
18856
20006
|
self.logger.debug("Updated traversal data -> %s", str(traversal_data))
|
|
18857
20007
|
|
|
18858
|
-
# Success = True, Traverse =
|
|
20008
|
+
# Success = True, Traverse = True
|
|
18859
20009
|
# We have traverse = True because we need to
|
|
18860
20010
|
# keep traversing into the workspace folders.
|
|
18861
20011
|
return (True, True)
|
|
@@ -18882,8 +20032,16 @@ class OTCS:
|
|
|
18882
20032
|
filter_item_data = kwargs.get("filter_item_data")
|
|
18883
20033
|
control_flags = kwargs.get("control_flags")
|
|
18884
20034
|
|
|
18885
|
-
if not traversal_data
|
|
18886
|
-
self.logger.error("Missing keyword
|
|
20035
|
+
if not traversal_data:
|
|
20036
|
+
self.logger.error("Missing keyword argument 'traversal_data' for executable in node item traversal!")
|
|
20037
|
+
return (False, False)
|
|
20038
|
+
|
|
20039
|
+
if not filter_item_data:
|
|
20040
|
+
self.logger.error("Missing keyword argument 'filter_item_data' for executable in node item traversal!")
|
|
20041
|
+
return (False, False)
|
|
20042
|
+
|
|
20043
|
+
if not control_flags:
|
|
20044
|
+
self.logger.error("Missing keyword argument 'control_flags' for executable in node item traversal!")
|
|
18887
20045
|
return (False, False)
|
|
18888
20046
|
|
|
18889
20047
|
node_id = self.get_result_value(response=node, key="id")
|
|
@@ -18918,7 +20076,7 @@ class OTCS:
|
|
|
18918
20076
|
categories = None
|
|
18919
20077
|
|
|
18920
20078
|
#
|
|
18921
|
-
# 2. Apply the defined filters to the current node to see
|
|
20079
|
+
# 2. Apply the defined item filters to the current node to see
|
|
18922
20080
|
# if we want to add it to the data frame as an item.
|
|
18923
20081
|
#
|
|
18924
20082
|
# If filter_item_in_workspace is false, then documents
|
|
@@ -18935,10 +20093,17 @@ class OTCS:
|
|
|
18935
20093
|
filter_category=filter_item_data["filter_item_category"],
|
|
18936
20094
|
filter_attributes=filter_item_data["filter_item_attributes"],
|
|
18937
20095
|
):
|
|
20096
|
+
self.logger.debug(
|
|
20097
|
+
"Node -> '%s' (%s) did not match item filter -> %s",
|
|
20098
|
+
node_name,
|
|
20099
|
+
node_id,
|
|
20100
|
+
str(filter_item_data),
|
|
20101
|
+
)
|
|
20102
|
+
|
|
18938
20103
|
# Success = False, Traverse = True
|
|
18939
20104
|
return (False, True)
|
|
18940
20105
|
|
|
18941
|
-
#
|
|
20106
|
+
# Debug output where we found the item (inside or outside of workspace):
|
|
18942
20107
|
if workspace_id:
|
|
18943
20108
|
self.logger.debug(
|
|
18944
20109
|
"Found %s item -> '%s' (%s) in depth -> %s inside workspace -> '%s' (%s).",
|
|
@@ -18971,15 +20136,19 @@ class OTCS:
|
|
|
18971
20136
|
if control_flags["download_documents"] and (
|
|
18972
20137
|
not os.path.exists(file_path) or not control_flags["skip_existing_downloads"]
|
|
18973
20138
|
):
|
|
18974
|
-
|
|
18975
|
-
|
|
18976
|
-
#
|
|
20139
|
+
mime_type = self.get_result_value(response=node, key="mime_type")
|
|
20140
|
+
extract_after_download = mime_type == "application/x-zip-compressed" and extract_zip
|
|
18977
20141
|
self.logger.debug(
|
|
18978
|
-
"Downloading file -> '%s'...",
|
|
20142
|
+
"Downloading document -> '%s' (%s) to temp file -> '%s'%s...",
|
|
20143
|
+
node_name,
|
|
20144
|
+
mime_type,
|
|
18979
20145
|
file_path,
|
|
20146
|
+
" and extracting it after download" if extract_after_download else "",
|
|
18980
20147
|
)
|
|
18981
20148
|
|
|
18982
|
-
|
|
20149
|
+
#
|
|
20150
|
+
# Start asynchronous Download Thread:
|
|
20151
|
+
#
|
|
18983
20152
|
thread = threading.Thread(
|
|
18984
20153
|
target=self.download_document_multi_threading,
|
|
18985
20154
|
args=(node_id, file_path, extract_after_download),
|
|
@@ -18989,7 +20158,8 @@ class OTCS:
|
|
|
18989
20158
|
download_threads.append(thread)
|
|
18990
20159
|
else:
|
|
18991
20160
|
self.logger.debug(
|
|
18992
|
-
"
|
|
20161
|
+
"Document -> '%s' has been downloaded to file -> %s before or download is not requested. Skipping download...",
|
|
20162
|
+
node_name,
|
|
18993
20163
|
file_path,
|
|
18994
20164
|
)
|
|
18995
20165
|
# end if document
|
|
@@ -18998,26 +20168,36 @@ class OTCS:
|
|
|
18998
20168
|
# Construct a dictionary 'row' that we will add
|
|
18999
20169
|
# to the resulting data frame:
|
|
19000
20170
|
#
|
|
19001
|
-
row = {}
|
|
19002
|
-
|
|
19003
|
-
|
|
19004
|
-
|
|
19005
|
-
|
|
19006
|
-
|
|
19007
|
-
|
|
20171
|
+
row = {"type": "item"}
|
|
20172
|
+
if workspaces:
|
|
20173
|
+
# First we include some key workspace data to associate
|
|
20174
|
+
# the item with the workspace:
|
|
20175
|
+
row["workspace_type"] = workspace_type
|
|
20176
|
+
row["workspace_id"] = workspace_id
|
|
20177
|
+
row["workspace_name"] = workspace_name
|
|
20178
|
+
row["workspace_description"] = workspace_description
|
|
19008
20179
|
# Then add item specific data:
|
|
19009
20180
|
row["item_id"] = str(node_id)
|
|
19010
20181
|
row["item_type"] = node_type
|
|
19011
20182
|
row["item_name"] = node_name
|
|
19012
20183
|
row["item_description"] = node_description
|
|
19013
|
-
|
|
20184
|
+
row["item_path"] = []
|
|
20185
|
+
# We take the part of folder path which is inside the workspace
|
|
19014
20186
|
# as the item path:
|
|
19015
|
-
|
|
19016
|
-
|
|
19017
|
-
|
|
19018
|
-
|
|
19019
|
-
|
|
19020
|
-
|
|
20187
|
+
if (
|
|
20188
|
+
folder_path and workspace_name and workspace_name in folder_path
|
|
20189
|
+
): # check if folder_path is not empty, this can happy if document items are the workspace items
|
|
20190
|
+
try:
|
|
20191
|
+
# Item path are the list elements after the item that is the workspace name:
|
|
20192
|
+
row["item_path"] = folder_path[folder_path.index(workspace_name) + 1 :]
|
|
20193
|
+
except ValueError:
|
|
20194
|
+
self.logger.warning(
|
|
20195
|
+
"Cannot find workspace name -> '%s' in folder path -> %s while processing -> '%s' (%s)!",
|
|
20196
|
+
workspace_name,
|
|
20197
|
+
folder_path,
|
|
20198
|
+
node_name,
|
|
20199
|
+
node_id,
|
|
20200
|
+
)
|
|
19021
20201
|
row["item_download_name"] = str(node_id) if node_type == self.ITEM_TYPE_DOCUMENT else ""
|
|
19022
20202
|
row["item_mime_type"] = (
|
|
19023
20203
|
self.get_result_value(response=node, key="mime_type") if node_type == self.ITEM_TYPE_DOCUMENT else ""
|
|
@@ -19025,10 +20205,10 @@ class OTCS:
|
|
|
19025
20205
|
# URL specific data:
|
|
19026
20206
|
row["item_url"] = self.get_result_value(response=node, key="url") if node_type == self.ITEM_TYPE_URL else ""
|
|
19027
20207
|
if item_metadata and categories and categories["results"]:
|
|
19028
|
-
# Add columns for
|
|
20208
|
+
# Add columns for item node categories have been determined above.
|
|
19029
20209
|
self.add_attribute_columns(row=row, categories=categories, prefix="item_cat_")
|
|
19030
20210
|
|
|
19031
|
-
# Now we add the row to the Pandas Data Frame in the Data class:
|
|
20211
|
+
# Now we add the item row to the Pandas Data Frame in the Data class:
|
|
19032
20212
|
self.logger.info(
|
|
19033
20213
|
"Adding %s -> '%s' (%s) to data frame...",
|
|
19034
20214
|
"document" if node_type == self.ITEM_TYPE_DOCUMENT else "URL",
|
|
@@ -19038,7 +20218,9 @@ class OTCS:
|
|
|
19038
20218
|
with self._data.lock():
|
|
19039
20219
|
self._data.append(row)
|
|
19040
20220
|
|
|
19041
|
-
|
|
20221
|
+
# Success = True, Traverse = False
|
|
20222
|
+
# We have traverse = False because document or URL items have no sub-items.
|
|
20223
|
+
return (True, False)
|
|
19042
20224
|
|
|
19043
20225
|
# end check_node_item()
|
|
19044
20226
|
|
|
@@ -19076,19 +20258,37 @@ class OTCS:
|
|
|
19076
20258
|
"extract_zip": extract_zip,
|
|
19077
20259
|
}
|
|
19078
20260
|
|
|
20261
|
+
#
|
|
20262
|
+
# Define the list of executables to call for each node:
|
|
20263
|
+
#
|
|
20264
|
+
executables = []
|
|
20265
|
+
if workspaces:
|
|
20266
|
+
executables.append(check_node_workspace)
|
|
20267
|
+
if items:
|
|
20268
|
+
executables.append(check_node_item)
|
|
20269
|
+
if not executables:
|
|
20270
|
+
self.logger.error("Neither workspaces nor items are requested to be loaded. Nothing to do!")
|
|
20271
|
+
return None
|
|
20272
|
+
|
|
19079
20273
|
#
|
|
19080
20274
|
# Start the traversal of the nodes:
|
|
19081
20275
|
#
|
|
19082
20276
|
result = self.traverse_node_parallel(
|
|
19083
20277
|
node=node_id,
|
|
20278
|
+
# For each node we call these executables in this order to check if
|
|
20279
|
+
# the node should be added to the resulting data frame:
|
|
19084
20280
|
executables=[check_node_exclusions, check_node_workspace, check_node_item],
|
|
20281
|
+
workers=workers, # number of worker threads
|
|
19085
20282
|
exclude_node_ids=exclude_node_ids,
|
|
19086
20283
|
filter_workspace_data=filter_workspace_data,
|
|
19087
20284
|
filter_item_data=filter_item_data,
|
|
19088
20285
|
control_flags=control_flags,
|
|
19089
|
-
workers=workers, # number of worker threads
|
|
19090
20286
|
)
|
|
19091
20287
|
|
|
20288
|
+
# Wait for all download threads to complete:
|
|
20289
|
+
for thread in download_threads:
|
|
20290
|
+
thread.join()
|
|
20291
|
+
|
|
19092
20292
|
return result
|
|
19093
20293
|
|
|
19094
20294
|
# end method definition
|
|
@@ -19177,7 +20377,7 @@ class OTCS:
|
|
|
19177
20377
|
}
|
|
19178
20378
|
if message_override:
|
|
19179
20379
|
message.update(message_override)
|
|
19180
|
-
self.logger.
|
|
20380
|
+
self.logger.debug(
|
|
19181
20381
|
"Start Content Aviator embedding on -> '%s' (%s), type -> %s, crawl -> %s, wait for completion -> %s, workspaces -> %s, documents -> %s, images -> %s",
|
|
19182
20382
|
node_properties["name"],
|
|
19183
20383
|
node_properties["id"],
|
|
@@ -19188,7 +20388,7 @@ class OTCS:
|
|
|
19188
20388
|
document_metadata,
|
|
19189
20389
|
images,
|
|
19190
20390
|
)
|
|
19191
|
-
self.logger.debug("Sending WebSocket message -> %s", message)
|
|
20391
|
+
self.logger.debug("Sending WebSocket message -> %s...", message)
|
|
19192
20392
|
await websocket.send(message=json.dumps(message))
|
|
19193
20393
|
|
|
19194
20394
|
# Continuously listen for messages
|
|
@@ -19295,3 +20495,170 @@ class OTCS:
|
|
|
19295
20495
|
return success
|
|
19296
20496
|
|
|
19297
20497
|
# end method definition
|
|
20498
|
+
|
|
20499
|
+
def _get_document_template_raw(self, workspace_id: int) -> ET.Element | None:
|
|
20500
|
+
"""Get the raw template XML payload from a workspace.
|
|
20501
|
+
|
|
20502
|
+
Args:
|
|
20503
|
+
workspace_id (int):
|
|
20504
|
+
The ID of the workspace to generate the document from.
|
|
20505
|
+
|
|
20506
|
+
Returns:
|
|
20507
|
+
ET.Element | None:
|
|
20508
|
+
The XML Element with the payload to initiate a document generation, or None if an error occurred
|
|
20509
|
+
|
|
20510
|
+
"""
|
|
20511
|
+
|
|
20512
|
+
request_url = self.config()["csUrl"]
|
|
20513
|
+
|
|
20514
|
+
request_header = self.request_form_header()
|
|
20515
|
+
request_header["referer"] = "http://localhost"
|
|
20516
|
+
|
|
20517
|
+
data = {
|
|
20518
|
+
"func": "xecmpfdocgen.PowerDocsPayload",
|
|
20519
|
+
"wsId": str(workspace_id),
|
|
20520
|
+
"hideHeader": "true",
|
|
20521
|
+
"source": "CreateDocument",
|
|
20522
|
+
}
|
|
20523
|
+
|
|
20524
|
+
self.logger.debug(
|
|
20525
|
+
"Get document templates for workspace with ID -> %d; calling -> %s",
|
|
20526
|
+
workspace_id,
|
|
20527
|
+
request_url,
|
|
20528
|
+
)
|
|
20529
|
+
|
|
20530
|
+
response = self.do_request(
|
|
20531
|
+
url=request_url,
|
|
20532
|
+
method="POST",
|
|
20533
|
+
headers=request_header,
|
|
20534
|
+
data=data,
|
|
20535
|
+
timeout=None,
|
|
20536
|
+
failure_message="Failed to get document templates for workspace with ID -> {}".format(workspace_id),
|
|
20537
|
+
parse_request_response=False,
|
|
20538
|
+
)
|
|
20539
|
+
|
|
20540
|
+
if response is None:
|
|
20541
|
+
return None
|
|
20542
|
+
|
|
20543
|
+
try:
|
|
20544
|
+
text = response.text
|
|
20545
|
+
match = re.search(r'<textarea[^>]*name=["\']documentgeneration["\'][^>]*>(.*?)</textarea>', text, re.DOTALL)
|
|
20546
|
+
textarea_content = match.group(1).strip() if match else ""
|
|
20547
|
+
textarea_content = html.unescape(textarea_content)
|
|
20548
|
+
|
|
20549
|
+
# Load Payload into XML object
|
|
20550
|
+
# payload is an XML formatted string, load it into an XML object for further processing
|
|
20551
|
+
|
|
20552
|
+
root = ET.Element("documentgeneration", format="pdf")
|
|
20553
|
+
root.append(ET.fromstring(textarea_content))
|
|
20554
|
+
except (ET.ParseError, AttributeError) as exc:
|
|
20555
|
+
self.logger.error(
|
|
20556
|
+
"Cannot parse document template XML payload for workspace with ID -> %d! Error -> %s",
|
|
20557
|
+
workspace_id,
|
|
20558
|
+
exc,
|
|
20559
|
+
)
|
|
20560
|
+
return None
|
|
20561
|
+
else:
|
|
20562
|
+
return root
|
|
20563
|
+
|
|
20564
|
+
# end method definition
|
|
20565
|
+
|
|
20566
|
+
def get_document_template_names(self, workspace_id: int, root: ET.Element | None = None) -> list[str] | None:
|
|
20567
|
+
"""Get the list of available template names from a workspace.
|
|
20568
|
+
|
|
20569
|
+
Args:
|
|
20570
|
+
workspace_id (int):
|
|
20571
|
+
The ID of the workspace to generate the document from.
|
|
20572
|
+
root (ET.Element | None, optional):
|
|
20573
|
+
The XML Element with the payload to initiate a document generation.
|
|
20574
|
+
|
|
20575
|
+
Returns:
|
|
20576
|
+
list[str] | None:
|
|
20577
|
+
A list of template names available in the workspace, or None if an error occurred.
|
|
20578
|
+
|
|
20579
|
+
"""
|
|
20580
|
+
|
|
20581
|
+
if root is None:
|
|
20582
|
+
root = self._get_document_template_raw(workspace_id=workspace_id)
|
|
20583
|
+
if root is None:
|
|
20584
|
+
self.logger.error(
|
|
20585
|
+
"Cannot get document templates for workspace with ID -> %d",
|
|
20586
|
+
workspace_id,
|
|
20587
|
+
)
|
|
20588
|
+
return None
|
|
20589
|
+
template_names = [item.text for item in root.findall("startup/processing/templates/template")]
|
|
20590
|
+
|
|
20591
|
+
return template_names
|
|
20592
|
+
|
|
20593
|
+
# end method definition
|
|
20594
|
+
|
|
20595
|
+
def get_document_template(
|
|
20596
|
+
self, workspace_id: int, template_name: str, input_values: dict | None = None
|
|
20597
|
+
) -> str | None:
|
|
20598
|
+
"""Get the template XML payload from a workspace and a given template name.
|
|
20599
|
+
|
|
20600
|
+
Args:
|
|
20601
|
+
workspace_id (int):
|
|
20602
|
+
The ID of the workspace to generate the document from.
|
|
20603
|
+
template_name (str):
|
|
20604
|
+
The name of the template to use for document generation.
|
|
20605
|
+
input_values (dict | None, optional):
|
|
20606
|
+
A dictionary with input values to replace in the template.
|
|
20607
|
+
|
|
20608
|
+
Returns:
|
|
20609
|
+
str | None:
|
|
20610
|
+
The XML string with the payload to initiate a document generation, or None if an error occurred.
|
|
20611
|
+
|
|
20612
|
+
"""
|
|
20613
|
+
|
|
20614
|
+
root = self._get_document_template_raw(workspace_id=workspace_id)
|
|
20615
|
+
if root is None:
|
|
20616
|
+
self.logger.error(
|
|
20617
|
+
"Cannot get document template for workspace with ID -> %d",
|
|
20618
|
+
workspace_id,
|
|
20619
|
+
)
|
|
20620
|
+
return None
|
|
20621
|
+
|
|
20622
|
+
template_names = self.get_document_template_names(workspace_id=workspace_id, root=root)
|
|
20623
|
+
|
|
20624
|
+
if template_name not in template_names:
|
|
20625
|
+
self.logger.error(
|
|
20626
|
+
"Template name -> '%s' not found in workspace with ID -> %d! Available templates are: %s",
|
|
20627
|
+
template_name,
|
|
20628
|
+
workspace_id,
|
|
20629
|
+
", ".join(template_names),
|
|
20630
|
+
)
|
|
20631
|
+
return None
|
|
20632
|
+
|
|
20633
|
+
# remove startup/processing
|
|
20634
|
+
startup = root.find("startup")
|
|
20635
|
+
# Find the existing application element and update its sysid attribute
|
|
20636
|
+
application = startup.find("application")
|
|
20637
|
+
application.set("sysid", "3adfd3a4-718f-4b9c-ac93-72efbcdf17f1")
|
|
20638
|
+
|
|
20639
|
+
processing = startup.find("processing")
|
|
20640
|
+
|
|
20641
|
+
# Clear processing information
|
|
20642
|
+
processing.clear()
|
|
20643
|
+
|
|
20644
|
+
modus = ET.SubElement(processing, "modus")
|
|
20645
|
+
modus.text = "local"
|
|
20646
|
+
editor = ET.SubElement(processing, "editor")
|
|
20647
|
+
editor.text = "false"
|
|
20648
|
+
template = ET.SubElement(processing, "template", type="Name")
|
|
20649
|
+
template.text = template_name
|
|
20650
|
+
channel = ET.SubElement(processing, "channel")
|
|
20651
|
+
channel.text = "save"
|
|
20652
|
+
|
|
20653
|
+
# Add static query information for userId and asOfDate
|
|
20654
|
+
if input_values:
|
|
20655
|
+
query = ET.SubElement(startup, "query", type="value")
|
|
20656
|
+
input_element = ET.SubElement(query, "input")
|
|
20657
|
+
|
|
20658
|
+
for column, value in input_values.items():
|
|
20659
|
+
value_element = ET.SubElement(input_element, "value", column=column)
|
|
20660
|
+
value_element.text = value
|
|
20661
|
+
|
|
20662
|
+
payload = ET.tostring(root, encoding="utf8").decode("utf8")
|
|
20663
|
+
|
|
20664
|
+
return payload
|