pyxecm 3.0.0__py3-none-any.whl → 3.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +870 -67
- pyxecm/otcs.py +1567 -280
- pyxecm/otds.py +332 -153
- pyxecm/otkd.py +4 -4
- pyxecm/otmm.py +1 -1
- pyxecm/otpd.py +246 -30
- pyxecm-3.1.0.dist-info/METADATA +127 -0
- pyxecm-3.1.0.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 +164 -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 +98 -48
- pyxecm_customizer/customizer.py +43 -25
- pyxecm_customizer/guidewire.py +422 -8
- pyxecm_customizer/k8s.py +23 -27
- pyxecm_customizer/knowledge_graph.py +501 -20
- pyxecm_customizer/m365.py +45 -44
- pyxecm_customizer/payload.py +1684 -1159
- 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.0.dist-info/METADATA +0 -48
- pyxecm-3.0.0.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.0.dist-info → pyxecm-3.1.0.dist-info}/WHEEL +0 -0
- {pyxecm-3.0.0.dist-info → pyxecm-3.1.0.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
|
|
@@ -567,6 +546,34 @@ class OTCS:
|
|
|
567
546
|
|
|
568
547
|
# end method definition
|
|
569
548
|
|
|
549
|
+
def otcs_ticket_hashed(self) -> str | None:
|
|
550
|
+
"""Return the hashed OTCS ticket.
|
|
551
|
+
|
|
552
|
+
Returns:
|
|
553
|
+
str | None:
|
|
554
|
+
The hashed OTCS ticket (which may be None).
|
|
555
|
+
|
|
556
|
+
"""
|
|
557
|
+
|
|
558
|
+
if not self._otcs_ticket:
|
|
559
|
+
return None
|
|
560
|
+
|
|
561
|
+
# Encode the input string before hashing
|
|
562
|
+
encoded_string = self._otcs_ticket.encode("utf-8")
|
|
563
|
+
|
|
564
|
+
# Create a new SHA-512 hash object
|
|
565
|
+
sha512 = hashlib.sha512()
|
|
566
|
+
|
|
567
|
+
# Update the hash object with the input string
|
|
568
|
+
sha512.update(encoded_string)
|
|
569
|
+
|
|
570
|
+
# Get the hexadecimal representation of the hash
|
|
571
|
+
hashed_output = sha512.hexdigest()
|
|
572
|
+
|
|
573
|
+
return hashed_output
|
|
574
|
+
|
|
575
|
+
# end method definition
|
|
576
|
+
|
|
570
577
|
def set_otcs_ticket(self, ticket: str) -> None:
|
|
571
578
|
"""Set the OTCS ticket.
|
|
572
579
|
|
|
@@ -593,6 +600,19 @@ class OTCS:
|
|
|
593
600
|
|
|
594
601
|
# end method definition
|
|
595
602
|
|
|
603
|
+
def set_otds_token(self, token: str) -> None:
|
|
604
|
+
"""Set the OTDS token.
|
|
605
|
+
|
|
606
|
+
Args:
|
|
607
|
+
token (str):
|
|
608
|
+
The new OTDS token.
|
|
609
|
+
|
|
610
|
+
"""
|
|
611
|
+
|
|
612
|
+
self._otds_token = token
|
|
613
|
+
|
|
614
|
+
# end method definition
|
|
615
|
+
|
|
596
616
|
def credentials(self) -> dict:
|
|
597
617
|
"""Get credentials (username + password).
|
|
598
618
|
|
|
@@ -903,7 +923,7 @@ class OTCS:
|
|
|
903
923
|
data: dict | None = None,
|
|
904
924
|
json_data: dict | None = None,
|
|
905
925
|
files: dict | None = None,
|
|
906
|
-
timeout:
|
|
926
|
+
timeout: float | None = REQUEST_TIMEOUT,
|
|
907
927
|
show_error: bool = True,
|
|
908
928
|
show_warning: bool = False,
|
|
909
929
|
warning_message: str = "",
|
|
@@ -931,7 +951,7 @@ class OTCS:
|
|
|
931
951
|
Dictionary of {"name": file-tuple} for multipart encoding upload.
|
|
932
952
|
The file-tuple can be a 2-tuple ("filename", fileobj) or a 3-tuple
|
|
933
953
|
("filename", fileobj, "content_type").
|
|
934
|
-
timeout (
|
|
954
|
+
timeout (float | None, optional):
|
|
935
955
|
Timeout for the request in seconds. Defaults to REQUEST_TIMEOUT.
|
|
936
956
|
show_error (bool, optional):
|
|
937
957
|
Whether or not an error should be logged in case of a failed REST call.
|
|
@@ -1106,8 +1126,11 @@ class OTCS:
|
|
|
1106
1126
|
except requests.exceptions.ConnectionError:
|
|
1107
1127
|
if retries <= max_retries:
|
|
1108
1128
|
self.logger.warning(
|
|
1109
|
-
"Connection error
|
|
1110
|
-
|
|
1129
|
+
"Connection error (%s)! Retrying in %d seconds... %d/%d",
|
|
1130
|
+
url,
|
|
1131
|
+
REQUEST_RETRY_DELAY,
|
|
1132
|
+
retries,
|
|
1133
|
+
max_retries,
|
|
1111
1134
|
)
|
|
1112
1135
|
retries += 1
|
|
1113
1136
|
|
|
@@ -1226,9 +1249,7 @@ class OTCS:
|
|
|
1226
1249
|
|
|
1227
1250
|
"""
|
|
1228
1251
|
|
|
1229
|
-
if not response:
|
|
1230
|
-
return None
|
|
1231
|
-
if "results" not in response:
|
|
1252
|
+
if not response or "results" not in response:
|
|
1232
1253
|
return None
|
|
1233
1254
|
|
|
1234
1255
|
results = response["results"]
|
|
@@ -1408,7 +1429,7 @@ class OTCS:
|
|
|
1408
1429
|
|
|
1409
1430
|
def get_result_value(
|
|
1410
1431
|
self,
|
|
1411
|
-
response: dict,
|
|
1432
|
+
response: dict | None,
|
|
1412
1433
|
key: str,
|
|
1413
1434
|
index: int = 0,
|
|
1414
1435
|
property_name: str = "properties",
|
|
@@ -1421,8 +1442,8 @@ class OTCS:
|
|
|
1421
1442
|
developer.opentext.com.
|
|
1422
1443
|
|
|
1423
1444
|
Args:
|
|
1424
|
-
response (dict):
|
|
1425
|
-
REST API response object.
|
|
1445
|
+
response (dict | None):
|
|
1446
|
+
REST API response object. None is also handled.
|
|
1426
1447
|
key (str):
|
|
1427
1448
|
Key to find (e.g., "id", "name").
|
|
1428
1449
|
index (int, optional):
|
|
@@ -1435,7 +1456,7 @@ class OTCS:
|
|
|
1435
1456
|
Whether an error or just a warning should be logged.
|
|
1436
1457
|
|
|
1437
1458
|
Returns:
|
|
1438
|
-
str:
|
|
1459
|
+
str | None:
|
|
1439
1460
|
Value of the item with the given key, or None if no value is found.
|
|
1440
1461
|
|
|
1441
1462
|
"""
|
|
@@ -1521,16 +1542,22 @@ class OTCS:
|
|
|
1521
1542
|
data = results[index]["data"]
|
|
1522
1543
|
if isinstance(data, dict):
|
|
1523
1544
|
# data is a dict - we don't need index value:
|
|
1524
|
-
properties = data
|
|
1545
|
+
properties = data.get(property_name)
|
|
1525
1546
|
elif isinstance(data, list):
|
|
1526
1547
|
# data is a list - this has typically just one item, so we use 0 as index
|
|
1527
|
-
properties = data[0]
|
|
1548
|
+
properties = data[0].get(property_name)
|
|
1528
1549
|
else:
|
|
1529
1550
|
self.logger.error(
|
|
1530
1551
|
"Data needs to be a list or dict but it is -> %s",
|
|
1531
1552
|
str(type(data)),
|
|
1532
1553
|
)
|
|
1533
1554
|
return None
|
|
1555
|
+
if not properties:
|
|
1556
|
+
self.logger.error(
|
|
1557
|
+
"No properties found in data -> %s",
|
|
1558
|
+
str(data),
|
|
1559
|
+
)
|
|
1560
|
+
return None
|
|
1534
1561
|
if key not in properties:
|
|
1535
1562
|
if show_error:
|
|
1536
1563
|
self.logger.error("Key -> '%s' is not in result properties!", key)
|
|
@@ -1669,8 +1696,8 @@ class OTCS:
|
|
|
1669
1696
|
Defaults to "data".
|
|
1670
1697
|
|
|
1671
1698
|
Returns:
|
|
1672
|
-
|
|
1673
|
-
|
|
1699
|
+
iter:
|
|
1700
|
+
Iterator object for iterating through the values.
|
|
1674
1701
|
|
|
1675
1702
|
"""
|
|
1676
1703
|
|
|
@@ -1835,6 +1862,31 @@ class OTCS:
|
|
|
1835
1862
|
|
|
1836
1863
|
request_url = self.config()["authenticationUrl"]
|
|
1837
1864
|
|
|
1865
|
+
if self._otds_token and not revalidate:
|
|
1866
|
+
self.logger.debug(
|
|
1867
|
+
"Requesting OTCS ticket with existing OTDS token; calling -> %s",
|
|
1868
|
+
request_url,
|
|
1869
|
+
)
|
|
1870
|
+
# Add the OTDS token to the request headers:
|
|
1871
|
+
request_header = REQUEST_FORM_HEADERS | {"Authorization": f"Bearer {self._otds_token}"}
|
|
1872
|
+
|
|
1873
|
+
try:
|
|
1874
|
+
response = requests.get(
|
|
1875
|
+
url=request_url,
|
|
1876
|
+
headers=request_header,
|
|
1877
|
+
timeout=10,
|
|
1878
|
+
)
|
|
1879
|
+
if response.ok:
|
|
1880
|
+
# read the ticket from the response header:
|
|
1881
|
+
otcs_ticket = response.headers.get("OTCSTicket")
|
|
1882
|
+
|
|
1883
|
+
except requests.exceptions.RequestException as exception:
|
|
1884
|
+
self.logger.warning(
|
|
1885
|
+
"Unable to connect to -> %s; error -> %s",
|
|
1886
|
+
request_url,
|
|
1887
|
+
str(exception),
|
|
1888
|
+
)
|
|
1889
|
+
|
|
1838
1890
|
if self._otds_ticket and not revalidate:
|
|
1839
1891
|
self.logger.debug(
|
|
1840
1892
|
"Requesting OTCS ticket with existing OTDS ticket; calling -> %s",
|
|
@@ -2138,7 +2190,8 @@ class OTCS:
|
|
|
2138
2190
|
"""Apply Content Server administration settings from XML file.
|
|
2139
2191
|
|
|
2140
2192
|
Args:
|
|
2141
|
-
xml_file_path (str):
|
|
2193
|
+
xml_file_path (str):
|
|
2194
|
+
The fully qualified path to the XML settings file.
|
|
2142
2195
|
|
|
2143
2196
|
Returns:
|
|
2144
2197
|
dict | None:
|
|
@@ -2178,7 +2231,7 @@ class OTCS:
|
|
|
2178
2231
|
headers=request_header,
|
|
2179
2232
|
files=llconfig_file,
|
|
2180
2233
|
timeout=None,
|
|
2181
|
-
success_message="Admin settings in file -> '{}' have been applied".format(
|
|
2234
|
+
success_message="Admin settings in file -> '{}' have been applied.".format(
|
|
2182
2235
|
xml_file_path,
|
|
2183
2236
|
),
|
|
2184
2237
|
failure_message="Failed to import settings file -> '{}'".format(
|
|
@@ -2733,7 +2786,13 @@ class OTCS:
|
|
|
2733
2786
|
|
|
2734
2787
|
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="get_current_user")
|
|
2735
2788
|
def get_current_user(self) -> dict | None:
|
|
2736
|
-
"""Get the current authenticated user.
|
|
2789
|
+
"""Get the current authenticated user.
|
|
2790
|
+
|
|
2791
|
+
Returns:
|
|
2792
|
+
dict | None:
|
|
2793
|
+
Information for the current (authenticated) user.
|
|
2794
|
+
|
|
2795
|
+
"""
|
|
2737
2796
|
|
|
2738
2797
|
request_url = self.config()["authenticationUrl"]
|
|
2739
2798
|
|
|
@@ -2749,7 +2808,7 @@ class OTCS:
|
|
|
2749
2808
|
method="GET",
|
|
2750
2809
|
headers=request_header,
|
|
2751
2810
|
timeout=None,
|
|
2752
|
-
failure_message="Failed to get current user
|
|
2811
|
+
failure_message="Failed to get current user",
|
|
2753
2812
|
)
|
|
2754
2813
|
|
|
2755
2814
|
# end method definition
|
|
@@ -2764,6 +2823,7 @@ class OTCS:
|
|
|
2764
2823
|
email: str,
|
|
2765
2824
|
title: str,
|
|
2766
2825
|
base_group: int,
|
|
2826
|
+
phone: str = "",
|
|
2767
2827
|
privileges: list | None = None,
|
|
2768
2828
|
user_type: int = 0,
|
|
2769
2829
|
) -> dict | None:
|
|
@@ -2784,7 +2844,9 @@ class OTCS:
|
|
|
2784
2844
|
The title of the user.
|
|
2785
2845
|
base_group (int):
|
|
2786
2846
|
The base group id of the user (e.g. department)
|
|
2787
|
-
|
|
2847
|
+
phone (str, optional):
|
|
2848
|
+
The business phone number of the user.
|
|
2849
|
+
privileges (list | None, optional):
|
|
2788
2850
|
Possible values are Login, Public Access, Content Manager,
|
|
2789
2851
|
Modify Users, Modify Groups, User Admin Rights,
|
|
2790
2852
|
Grant Discovery, System Admin Rights
|
|
@@ -2808,6 +2870,7 @@ class OTCS:
|
|
|
2808
2870
|
"first_name": first_name,
|
|
2809
2871
|
"last_name": last_name,
|
|
2810
2872
|
"business_email": email,
|
|
2873
|
+
"business_phone": phone,
|
|
2811
2874
|
"title": title,
|
|
2812
2875
|
"group_id": base_group,
|
|
2813
2876
|
"privilege_login": ("Login" in privileges),
|
|
@@ -2987,11 +3050,14 @@ class OTCS:
|
|
|
2987
3050
|
"""Update a user with a profile photo (which must be an existing node).
|
|
2988
3051
|
|
|
2989
3052
|
Args:
|
|
2990
|
-
user_id (int):
|
|
2991
|
-
|
|
3053
|
+
user_id (int):
|
|
3054
|
+
The ID of the user.
|
|
3055
|
+
photo_id (int):
|
|
3056
|
+
The node ID of the photo.
|
|
2992
3057
|
|
|
2993
3058
|
Returns:
|
|
2994
|
-
dict | None:
|
|
3059
|
+
dict | None:
|
|
3060
|
+
Node information or None if photo node is not found.
|
|
2995
3061
|
|
|
2996
3062
|
"""
|
|
2997
3063
|
|
|
@@ -3027,10 +3093,12 @@ class OTCS:
|
|
|
3027
3093
|
that was introduced with version 23.4.
|
|
3028
3094
|
|
|
3029
3095
|
Args:
|
|
3030
|
-
user_name (str):
|
|
3096
|
+
user_name (str):
|
|
3097
|
+
The user to test (login name) for proxy.
|
|
3031
3098
|
|
|
3032
3099
|
Returns:
|
|
3033
|
-
bool:
|
|
3100
|
+
bool:
|
|
3101
|
+
True is user is proxy of current user. False if not.
|
|
3034
3102
|
|
|
3035
3103
|
"""
|
|
3036
3104
|
|
|
@@ -3956,7 +4024,7 @@ class OTCS:
|
|
|
3956
4024
|
method="GET",
|
|
3957
4025
|
headers=request_header,
|
|
3958
4026
|
timeout=REQUEST_TIMEOUT,
|
|
3959
|
-
failure_message="Failed to get system usage privileges
|
|
4027
|
+
failure_message="Failed to get system usage privileges",
|
|
3960
4028
|
)
|
|
3961
4029
|
|
|
3962
4030
|
if response:
|
|
@@ -4160,7 +4228,7 @@ class OTCS:
|
|
|
4160
4228
|
method="GET",
|
|
4161
4229
|
headers=request_header,
|
|
4162
4230
|
timeout=REQUEST_TIMEOUT,
|
|
4163
|
-
failure_message="Failed to get system usage privileges
|
|
4231
|
+
failure_message="Failed to get system usage privileges",
|
|
4164
4232
|
)
|
|
4165
4233
|
|
|
4166
4234
|
if response:
|
|
@@ -4306,9 +4374,9 @@ class OTCS:
|
|
|
4306
4374
|
def get_node(
|
|
4307
4375
|
self,
|
|
4308
4376
|
node_id: int,
|
|
4309
|
-
fields:
|
|
4377
|
+
fields: str | list = "properties", # per default we just get the most important information
|
|
4310
4378
|
metadata: bool = False,
|
|
4311
|
-
timeout:
|
|
4379
|
+
timeout: float = REQUEST_TIMEOUT,
|
|
4312
4380
|
) -> dict | None:
|
|
4313
4381
|
"""Get a node based on the node ID.
|
|
4314
4382
|
|
|
@@ -4335,7 +4403,7 @@ class OTCS:
|
|
|
4335
4403
|
The metadata will be returned under `results.metadata`, `metadata_map`,
|
|
4336
4404
|
and `metadata_order`.
|
|
4337
4405
|
Defaults to False.
|
|
4338
|
-
timeout (
|
|
4406
|
+
timeout (float, optional):
|
|
4339
4407
|
Timeout for the request in seconds. Defaults to `REQUEST_TIMEOUT`.
|
|
4340
4408
|
|
|
4341
4409
|
Returns:
|
|
@@ -5282,7 +5350,7 @@ class OTCS:
|
|
|
5282
5350
|
# end method definition
|
|
5283
5351
|
|
|
5284
5352
|
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="lookup_node")
|
|
5285
|
-
def
|
|
5353
|
+
def lookup_nodes(
|
|
5286
5354
|
self,
|
|
5287
5355
|
parent_node_id: int,
|
|
5288
5356
|
category: str,
|
|
@@ -5290,7 +5358,7 @@ class OTCS:
|
|
|
5290
5358
|
value: str,
|
|
5291
5359
|
attribute_set: str | None = None,
|
|
5292
5360
|
) -> dict | None:
|
|
5293
|
-
"""Lookup
|
|
5361
|
+
"""Lookup nodes under a parent node that have a specified value in a category attribute.
|
|
5294
5362
|
|
|
5295
5363
|
Args:
|
|
5296
5364
|
parent_node_id (int):
|
|
@@ -5306,10 +5374,12 @@ class OTCS:
|
|
|
5306
5374
|
|
|
5307
5375
|
Returns:
|
|
5308
5376
|
dict | None:
|
|
5309
|
-
Node wrapped in dictionary with "results" key or None if the REST API fails.
|
|
5377
|
+
Node(s) wrapped in dictionary with "results" key or None if the REST API fails.
|
|
5310
5378
|
|
|
5311
5379
|
"""
|
|
5312
5380
|
|
|
5381
|
+
results = {"results": []}
|
|
5382
|
+
|
|
5313
5383
|
# get_subnodes_iterator() returns a python generator that we use for iterating over all nodes
|
|
5314
5384
|
# in an efficient way avoiding to retrieve all nodes at once (which could be a large number):
|
|
5315
5385
|
for node in self.get_subnodes_iterator(
|
|
@@ -5392,25 +5462,29 @@ class OTCS:
|
|
|
5392
5462
|
if value in attribute_value:
|
|
5393
5463
|
# Create a "results" dict that is compatible with normal REST calls
|
|
5394
5464
|
# to not break get_result_value() method that may be called on the result:
|
|
5395
|
-
|
|
5465
|
+
results["results"].append(node)
|
|
5396
5466
|
elif value == attribute_value:
|
|
5397
5467
|
# Create a results dict that is compatible with normal REST calls
|
|
5398
5468
|
# to not break get_result_value() method that may be called on the result:
|
|
5399
|
-
|
|
5469
|
+
results["results"].append(node)
|
|
5470
|
+
# end if set_key
|
|
5400
5471
|
else:
|
|
5401
5472
|
key = prefix + attribute_id
|
|
5402
5473
|
attribute_value = cat_data.get(key)
|
|
5403
5474
|
if not attribute_value:
|
|
5404
|
-
|
|
5475
|
+
continue
|
|
5476
|
+
# Is it a multi-value attribute (i.e. a list of values)?
|
|
5405
5477
|
if isinstance(attribute_value, list):
|
|
5406
5478
|
if value in attribute_value:
|
|
5407
5479
|
# Create a "results" dict that is compatible with normal REST calls
|
|
5408
5480
|
# to not break get_result_value() method that may be called on the result:
|
|
5409
|
-
|
|
5481
|
+
results["results"].append(node)
|
|
5482
|
+
# If not a multi-value attribute, check for equality:
|
|
5410
5483
|
elif value == attribute_value:
|
|
5411
5484
|
# Create a results dict that is compatible with normal REST calls
|
|
5412
5485
|
# to not break get_result_value() method that may be called on the result:
|
|
5413
|
-
|
|
5486
|
+
results["results"].append(node)
|
|
5487
|
+
# end if set_key else
|
|
5414
5488
|
# end for cat_data, cat_schema in zip(data, schema)
|
|
5415
5489
|
# end for node in nodes
|
|
5416
5490
|
|
|
@@ -5422,7 +5496,7 @@ class OTCS:
|
|
|
5422
5496
|
parent_node_id,
|
|
5423
5497
|
)
|
|
5424
5498
|
|
|
5425
|
-
return
|
|
5499
|
+
return results if results["results"] else None
|
|
5426
5500
|
|
|
5427
5501
|
# end method definition
|
|
5428
5502
|
|
|
@@ -6322,14 +6396,14 @@ class OTCS:
|
|
|
6322
6396
|
def get_volume(
|
|
6323
6397
|
self,
|
|
6324
6398
|
volume_type: int,
|
|
6325
|
-
timeout:
|
|
6399
|
+
timeout: float = REQUEST_TIMEOUT,
|
|
6326
6400
|
) -> dict | None:
|
|
6327
6401
|
"""Get Volume information based on the volume type ID.
|
|
6328
6402
|
|
|
6329
6403
|
Args:
|
|
6330
6404
|
volume_type (int):
|
|
6331
6405
|
The ID of the volume type.
|
|
6332
|
-
timeout (
|
|
6406
|
+
timeout (float | None, optional):
|
|
6333
6407
|
The timeout for the request in seconds.
|
|
6334
6408
|
|
|
6335
6409
|
Returns:
|
|
@@ -6592,6 +6666,7 @@ class OTCS:
|
|
|
6592
6666
|
external_modify_date: str | None = None,
|
|
6593
6667
|
external_create_date: str | None = None,
|
|
6594
6668
|
extract_zip: bool = False,
|
|
6669
|
+
replace_existing: bool = False,
|
|
6595
6670
|
show_error: bool = True,
|
|
6596
6671
|
) -> dict | None:
|
|
6597
6672
|
"""Fetch a file from a URL or local filesystem and uploads it to a OTCS parent.
|
|
@@ -6633,6 +6708,9 @@ class OTCS:
|
|
|
6633
6708
|
extract_zip (bool, optional):
|
|
6634
6709
|
If True, automatically extract ZIP files and upload extracted directory. If False,
|
|
6635
6710
|
upload the unchanged Zip file.
|
|
6711
|
+
replace_existing (bool, optional):
|
|
6712
|
+
If True, replaces an existing file with the same name in the target folder. If False,
|
|
6713
|
+
the upload will fail if a file with the same name already exists.
|
|
6636
6714
|
show_error (bool, optional):
|
|
6637
6715
|
If True, treats the upload failure as an error. If False, no error is shown (useful if the file already exists).
|
|
6638
6716
|
|
|
@@ -6683,8 +6761,7 @@ class OTCS:
|
|
|
6683
6761
|
file_content = response.content
|
|
6684
6762
|
|
|
6685
6763
|
# 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
|
-
|
|
6764
|
+
# it and then defer the upload to upload_directory_to_parent():
|
|
6688
6765
|
elif os.path.exists(file_url) and (
|
|
6689
6766
|
((file_url.endswith(".zip") or mime_type == "application/x-zip-compressed") and extract_zip)
|
|
6690
6767
|
or os.path.isdir(file_url)
|
|
@@ -6692,6 +6769,7 @@ class OTCS:
|
|
|
6692
6769
|
return self.upload_directory_to_parent(
|
|
6693
6770
|
parent_id=parent_id,
|
|
6694
6771
|
file_path=file_url,
|
|
6772
|
+
replace_existing=replace_existing,
|
|
6695
6773
|
)
|
|
6696
6774
|
|
|
6697
6775
|
elif os.path.exists(file_url):
|
|
@@ -6786,7 +6864,7 @@ class OTCS:
|
|
|
6786
6864
|
# end method definition
|
|
6787
6865
|
|
|
6788
6866
|
@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:
|
|
6867
|
+
def upload_directory_to_parent(self, parent_id: int, file_path: str, replace_existing: bool = True) -> dict | None:
|
|
6790
6868
|
"""Upload a directory or an uncompressed zip file to Content Server.
|
|
6791
6869
|
|
|
6792
6870
|
IMPORTANT: if the path ends in a file then we assume it is a ZIP file!
|
|
@@ -6796,6 +6874,8 @@ class OTCS:
|
|
|
6796
6874
|
ID of the parent in Content Server.
|
|
6797
6875
|
file_path (str):
|
|
6798
6876
|
File system path to the directory or zip file.
|
|
6877
|
+
replace_existing (bool, optional):
|
|
6878
|
+
If True, existing files are replaced by uploading a new version.
|
|
6799
6879
|
|
|
6800
6880
|
Returns:
|
|
6801
6881
|
dict | None:
|
|
@@ -6893,21 +6973,25 @@ class OTCS:
|
|
|
6893
6973
|
response = self.upload_directory_to_parent(
|
|
6894
6974
|
parent_id=current_parent_id,
|
|
6895
6975
|
file_path=full_file_path,
|
|
6976
|
+
replace_existing=replace_existing,
|
|
6896
6977
|
)
|
|
6897
6978
|
if response and not first_response:
|
|
6898
6979
|
first_response = response.copy()
|
|
6899
6980
|
continue
|
|
6981
|
+
# Check if the file already exists:
|
|
6900
6982
|
response = self.get_node_by_parent_and_name(
|
|
6901
6983
|
parent_id=current_parent_id,
|
|
6902
6984
|
name=file_name,
|
|
6903
6985
|
)
|
|
6904
6986
|
if not response or not response["results"]:
|
|
6987
|
+
# File does not yet exist - upload new document:
|
|
6905
6988
|
response = self.upload_file_to_parent(
|
|
6906
6989
|
parent_id=current_parent_id,
|
|
6907
6990
|
file_url=full_file_path,
|
|
6908
6991
|
file_name=file_name,
|
|
6909
6992
|
)
|
|
6910
|
-
|
|
6993
|
+
elif replace_existing:
|
|
6994
|
+
# Document does already exist - upload a new version if replace existing is requested:
|
|
6911
6995
|
existing_document_id = self.get_result_value(
|
|
6912
6996
|
response=response,
|
|
6913
6997
|
key="id",
|
|
@@ -7429,6 +7513,8 @@ class OTCS:
|
|
|
7429
7513
|
node_id: int,
|
|
7430
7514
|
file_path: str,
|
|
7431
7515
|
version_number: str | int = "",
|
|
7516
|
+
chunk_size: int = 8192,
|
|
7517
|
+
overwrite: bool = True,
|
|
7432
7518
|
) -> bool:
|
|
7433
7519
|
"""Download a document from OTCS to local file system.
|
|
7434
7520
|
|
|
@@ -7440,6 +7526,12 @@ class OTCS:
|
|
|
7440
7526
|
version_number (str | int, optional):
|
|
7441
7527
|
The version of the document to download.
|
|
7442
7528
|
If version = "" then download the latest version.
|
|
7529
|
+
chunk_size (int, optional):
|
|
7530
|
+
The chunk size to use when downloading the document in bytes.
|
|
7531
|
+
Default is 8192 bytes.
|
|
7532
|
+
overwrite (bool, optional):
|
|
7533
|
+
If True, overwrite the file if it already exists. If False, do not overwrite
|
|
7534
|
+
and return False if the file already exists.
|
|
7443
7535
|
|
|
7444
7536
|
Returns:
|
|
7445
7537
|
bool:
|
|
@@ -7449,24 +7541,30 @@ class OTCS:
|
|
|
7449
7541
|
"""
|
|
7450
7542
|
|
|
7451
7543
|
if not version_number:
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7544
|
+
# we retrieve the latest version - using V1 REST API. V2 has issues here.:
|
|
7545
|
+
# request_url = self.config()["nodesUrlv2"] + "/" + str(node_id) + "/content"
|
|
7546
|
+
request_url = self.config()["nodesUrl"] + "/" + str(node_id) + "/content"
|
|
7547
|
+
self.logger.debug(
|
|
7548
|
+
"Download document with node ID -> %d (latest version); calling -> %s",
|
|
7549
|
+
node_id,
|
|
7550
|
+
request_url,
|
|
7551
|
+
)
|
|
7552
|
+
else:
|
|
7553
|
+
# we retrieve the given version - using V1 REST API. V2 has issues here.:
|
|
7554
|
+
# request_url = (
|
|
7555
|
+
# self.config()["nodesUrlv2"] + "/" + str(node_id) + "/versions/" + str(version_number) + "/content"
|
|
7556
|
+
# )
|
|
7557
|
+
request_url = (
|
|
7558
|
+
self.config()["nodesUrl"] + "/" + str(node_id) + "/versions/" + str(version_number) + "/content"
|
|
7559
|
+
)
|
|
7560
|
+
self.logger.debug(
|
|
7561
|
+
"Download document with node ID -> %d and version number -> %d; calling -> %s",
|
|
7562
|
+
node_id,
|
|
7563
|
+
version_number,
|
|
7564
|
+
request_url,
|
|
7565
|
+
)
|
|
7462
7566
|
request_header = self.request_download_header()
|
|
7463
7567
|
|
|
7464
|
-
self.logger.debug(
|
|
7465
|
-
"Download document with node ID -> %d; calling -> %s",
|
|
7466
|
-
node_id,
|
|
7467
|
-
request_url,
|
|
7468
|
-
)
|
|
7469
|
-
|
|
7470
7568
|
response = self.do_request(
|
|
7471
7569
|
url=request_url,
|
|
7472
7570
|
method="GET",
|
|
@@ -7482,6 +7580,18 @@ class OTCS:
|
|
|
7482
7580
|
if response is None:
|
|
7483
7581
|
return False
|
|
7484
7582
|
|
|
7583
|
+
total_size = int(response.headers["Content-Length"]) if "Content-Length" in response.headers else None
|
|
7584
|
+
|
|
7585
|
+
content_encoding = response.headers.get("Content-Encoding", "").lower()
|
|
7586
|
+
is_compressed = content_encoding in ("gzip", "deflate", "br")
|
|
7587
|
+
|
|
7588
|
+
if os.path.exists(file_path) and not overwrite:
|
|
7589
|
+
self.logger.warning(
|
|
7590
|
+
"File -> '%s' already exists and overwrite is set to False, not downloading document.",
|
|
7591
|
+
file_path,
|
|
7592
|
+
)
|
|
7593
|
+
return False
|
|
7594
|
+
|
|
7485
7595
|
directory = os.path.dirname(file_path)
|
|
7486
7596
|
if not os.path.exists(directory):
|
|
7487
7597
|
self.logger.info(
|
|
@@ -7490,12 +7600,28 @@ class OTCS:
|
|
|
7490
7600
|
)
|
|
7491
7601
|
os.makedirs(directory)
|
|
7492
7602
|
|
|
7603
|
+
bytes_downloaded = 0
|
|
7493
7604
|
try:
|
|
7494
7605
|
with open(file_path, "wb") as download_file:
|
|
7495
|
-
|
|
7496
|
-
|
|
7606
|
+
for chunk in response.iter_content(chunk_size=chunk_size):
|
|
7607
|
+
if chunk:
|
|
7608
|
+
download_file.write(chunk)
|
|
7609
|
+
bytes_downloaded += len(chunk)
|
|
7610
|
+
|
|
7611
|
+
except Exception as e:
|
|
7612
|
+
self.logger.error(
|
|
7613
|
+
"Error while writing content to file -> %s after %d bytes downloaded; error -> %s",
|
|
7614
|
+
file_path,
|
|
7615
|
+
bytes_downloaded,
|
|
7616
|
+
str(e),
|
|
7617
|
+
)
|
|
7618
|
+
return False
|
|
7619
|
+
|
|
7620
|
+
if total_size and not is_compressed and bytes_downloaded != total_size:
|
|
7497
7621
|
self.logger.error(
|
|
7498
|
-
"
|
|
7622
|
+
"Downloaded size (%d bytes) does not match expected size (%d bytes) for file -> '%s'",
|
|
7623
|
+
bytes_downloaded,
|
|
7624
|
+
total_size,
|
|
7499
7625
|
file_path,
|
|
7500
7626
|
)
|
|
7501
7627
|
return False
|
|
@@ -7592,9 +7718,11 @@ class OTCS:
|
|
|
7592
7718
|
search_term: str,
|
|
7593
7719
|
look_for: str = "complexQuery",
|
|
7594
7720
|
modifier: str = "",
|
|
7721
|
+
within: str = "all",
|
|
7595
7722
|
slice_id: int = 0,
|
|
7596
7723
|
query_id: int = 0,
|
|
7597
7724
|
template_id: int = 0,
|
|
7725
|
+
location_id: int | None = None,
|
|
7598
7726
|
limit: int = 100,
|
|
7599
7727
|
page: int = 1,
|
|
7600
7728
|
) -> dict | None:
|
|
@@ -7619,12 +7747,20 @@ class OTCS:
|
|
|
7619
7747
|
- 'wordendswith'
|
|
7620
7748
|
If not specified or any value other than these options is given,
|
|
7621
7749
|
it is ignored.
|
|
7750
|
+
within (str, optional):
|
|
7751
|
+
The scope of the search. Possible values are:
|
|
7752
|
+
- 'all': search in content and in metadata (default)
|
|
7753
|
+
- 'content': search only in document content
|
|
7754
|
+
- 'metadata': search only in item metadata
|
|
7622
7755
|
slice_id (int, optional):
|
|
7623
7756
|
The ID of an existing search slice.
|
|
7624
7757
|
query_id (int, optional):
|
|
7625
7758
|
The ID of a saved search query.
|
|
7626
7759
|
template_id (int, optional):
|
|
7627
7760
|
The ID of a saved search template.
|
|
7761
|
+
location_id (int | None, optional):
|
|
7762
|
+
The ID of a folder or workspace to start a search from here.
|
|
7763
|
+
None = unrestricted search (default).
|
|
7628
7764
|
limit (int, optional):
|
|
7629
7765
|
The maximum number of results to return. Default is 100.
|
|
7630
7766
|
page (int, optional):
|
|
@@ -7809,6 +7945,7 @@ class OTCS:
|
|
|
7809
7945
|
search_post_body = {
|
|
7810
7946
|
"where": search_term,
|
|
7811
7947
|
"lookfor": look_for,
|
|
7948
|
+
"within": within,
|
|
7812
7949
|
"page": page,
|
|
7813
7950
|
"limit": limit,
|
|
7814
7951
|
}
|
|
@@ -7821,6 +7958,8 @@ class OTCS:
|
|
|
7821
7958
|
search_post_body["query_id"] = query_id
|
|
7822
7959
|
if template_id > 0:
|
|
7823
7960
|
search_post_body["template_id"] = template_id
|
|
7961
|
+
if location_id is not None:
|
|
7962
|
+
search_post_body["location_id1"] = location_id
|
|
7824
7963
|
|
|
7825
7964
|
request_url = self.config()["searchUrl"]
|
|
7826
7965
|
request_header = self.request_form_header()
|
|
@@ -7847,10 +7986,13 @@ class OTCS:
|
|
|
7847
7986
|
search_term: str,
|
|
7848
7987
|
look_for: str = "complexQuery",
|
|
7849
7988
|
modifier: str = "",
|
|
7989
|
+
within: str = "all",
|
|
7850
7990
|
slice_id: int = 0,
|
|
7851
7991
|
query_id: int = 0,
|
|
7852
7992
|
template_id: int = 0,
|
|
7993
|
+
location_id: int | None = None,
|
|
7853
7994
|
page_size: int = 100,
|
|
7995
|
+
limit: int | None = None,
|
|
7854
7996
|
) -> iter:
|
|
7855
7997
|
"""Get an iterator object to traverse all search results for a given search.
|
|
7856
7998
|
|
|
@@ -7878,19 +8020,31 @@ class OTCS:
|
|
|
7878
8020
|
Defines a modifier for the search. Possible values are:
|
|
7879
8021
|
- 'synonymsof', 'relatedto', 'soundslike', 'wordbeginswith', 'wordendswith'.
|
|
7880
8022
|
If not specified or any value other than these options is given, it is ignored.
|
|
8023
|
+
within (str, optional):
|
|
8024
|
+
The scope of the search. Possible values are:
|
|
8025
|
+
- 'all': search in content and in metadata (default)
|
|
8026
|
+
- 'content': search only in document content
|
|
8027
|
+
- 'metadata': search only in item metadata
|
|
7881
8028
|
slice_id (int, optional):
|
|
7882
8029
|
The ID of an existing search slice.
|
|
7883
8030
|
query_id (int, optional):
|
|
7884
8031
|
The ID of a saved search query.
|
|
7885
8032
|
template_id (int, optional):
|
|
7886
8033
|
The ID of a saved search template.
|
|
8034
|
+
location_id (int | None, optional):
|
|
8035
|
+
The ID of a folder or workspace to start a search from here.
|
|
8036
|
+
None = unrestricted search (default).
|
|
7887
8037
|
page_size (int, optional):
|
|
7888
8038
|
The maximum number of results to return. Default is 100.
|
|
7889
|
-
For the iterator this
|
|
8039
|
+
For the iterator this is basically the chunk size.
|
|
8040
|
+
limit (int | None = None), optional):
|
|
8041
|
+
The maximum number of results to return in total.
|
|
8042
|
+
If None (default) all results are returned.
|
|
8043
|
+
If a number is provided only up to this number of results is returned.
|
|
7890
8044
|
|
|
7891
8045
|
Returns:
|
|
7892
|
-
|
|
7893
|
-
The search response
|
|
8046
|
+
iter:
|
|
8047
|
+
The search response iterator object.
|
|
7894
8048
|
|
|
7895
8049
|
"""
|
|
7896
8050
|
|
|
@@ -7899,9 +8053,11 @@ class OTCS:
|
|
|
7899
8053
|
search_term=search_term,
|
|
7900
8054
|
look_for=look_for,
|
|
7901
8055
|
modifier=modifier,
|
|
8056
|
+
within=within,
|
|
7902
8057
|
slice_id=slice_id,
|
|
7903
8058
|
query_id=query_id,
|
|
7904
8059
|
template_id=template_id,
|
|
8060
|
+
location_id=location_id,
|
|
7905
8061
|
limit=1,
|
|
7906
8062
|
page=1,
|
|
7907
8063
|
)
|
|
@@ -7912,6 +8068,9 @@ class OTCS:
|
|
|
7912
8068
|
return
|
|
7913
8069
|
|
|
7914
8070
|
number_of_results = response["collection"]["paging"]["total_count"]
|
|
8071
|
+
if limit and number_of_results > limit:
|
|
8072
|
+
number_of_results = limit
|
|
8073
|
+
|
|
7915
8074
|
if not number_of_results:
|
|
7916
8075
|
self.logger.debug(
|
|
7917
8076
|
"Search -> '%s' does not have results! Cannot iterate over results.",
|
|
@@ -7934,9 +8093,11 @@ class OTCS:
|
|
|
7934
8093
|
search_term=search_term,
|
|
7935
8094
|
look_for=look_for,
|
|
7936
8095
|
modifier=modifier,
|
|
8096
|
+
within=within,
|
|
7937
8097
|
slice_id=slice_id,
|
|
7938
8098
|
query_id=query_id,
|
|
7939
8099
|
template_id=template_id,
|
|
8100
|
+
location_id=location_id,
|
|
7940
8101
|
limit=page_size,
|
|
7941
8102
|
page=page,
|
|
7942
8103
|
)
|
|
@@ -7980,7 +8141,7 @@ class OTCS:
|
|
|
7980
8141
|
request_header = self.cookie()
|
|
7981
8142
|
|
|
7982
8143
|
self.logger.debug(
|
|
7983
|
-
"Get external system connection -> %s; calling -> %s",
|
|
8144
|
+
"Get external system connection -> '%s'; calling -> %s",
|
|
7984
8145
|
connection_name,
|
|
7985
8146
|
request_url,
|
|
7986
8147
|
)
|
|
@@ -8170,7 +8331,7 @@ class OTCS:
|
|
|
8170
8331
|
# end method definition
|
|
8171
8332
|
|
|
8172
8333
|
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="deploy_workbench")
|
|
8173
|
-
def deploy_workbench(self, workbench_id: int) -> dict | None:
|
|
8334
|
+
def deploy_workbench(self, workbench_id: int) -> tuple[dict | None, int]:
|
|
8174
8335
|
"""Deploy an existing Workbench.
|
|
8175
8336
|
|
|
8176
8337
|
Args:
|
|
@@ -8180,6 +8341,8 @@ class OTCS:
|
|
|
8180
8341
|
Returns:
|
|
8181
8342
|
dict | None:
|
|
8182
8343
|
The deploy response or None if the deployment fails.
|
|
8344
|
+
int:
|
|
8345
|
+
Error count. Should be 0 if fully successful.
|
|
8183
8346
|
|
|
8184
8347
|
Example response:
|
|
8185
8348
|
{
|
|
@@ -8227,8 +8390,12 @@ class OTCS:
|
|
|
8227
8390
|
),
|
|
8228
8391
|
)
|
|
8229
8392
|
|
|
8393
|
+
# Transport packages canalso partly fail to deploy.
|
|
8394
|
+
# For such cases we determine the number of errors.
|
|
8395
|
+
error_count = 0
|
|
8396
|
+
|
|
8230
8397
|
if not response or "results" not in response:
|
|
8231
|
-
return None
|
|
8398
|
+
return (None, 0)
|
|
8232
8399
|
|
|
8233
8400
|
try:
|
|
8234
8401
|
error_count = response["results"]["data"]["status"]["error_count"]
|
|
@@ -8239,7 +8406,7 @@ class OTCS:
|
|
|
8239
8406
|
else:
|
|
8240
8407
|
success_count = response["results"]["data"]["status"]["success_count"]
|
|
8241
8408
|
self.logger.info(
|
|
8242
|
-
"Transport successfully deployed %d workbench items",
|
|
8409
|
+
"Transport successfully deployed %d workbench items.",
|
|
8243
8410
|
success_count,
|
|
8244
8411
|
)
|
|
8245
8412
|
|
|
@@ -8254,7 +8421,7 @@ class OTCS:
|
|
|
8254
8421
|
except Exception as e:
|
|
8255
8422
|
self.logger.debug(str(e))
|
|
8256
8423
|
|
|
8257
|
-
return response
|
|
8424
|
+
return (response, error_count)
|
|
8258
8425
|
|
|
8259
8426
|
# end method definition
|
|
8260
8427
|
|
|
@@ -8309,11 +8476,18 @@ class OTCS:
|
|
|
8309
8476
|
if extractions is None:
|
|
8310
8477
|
extractions = []
|
|
8311
8478
|
|
|
8479
|
+
while not self.is_ready():
|
|
8480
|
+
self.logger.info(
|
|
8481
|
+
"OTCS is not ready. Cannot deploy transport -> '%s' to OTCS. Waiting 30 seconds and retry...",
|
|
8482
|
+
package_name,
|
|
8483
|
+
)
|
|
8484
|
+
time.sleep(30)
|
|
8485
|
+
|
|
8312
8486
|
# Preparation: get volume IDs for Transport Warehouse (root volume and Transport Packages)
|
|
8313
8487
|
response = self.get_volume(volume_type=self.VOLUME_TYPE_TRANSPORT_WAREHOUSE)
|
|
8314
8488
|
transport_root_volume_id = self.get_result_value(response=response, key="id")
|
|
8315
8489
|
if not transport_root_volume_id:
|
|
8316
|
-
self.logger.error("Failed to retrieve transport root volume")
|
|
8490
|
+
self.logger.error("Failed to retrieve transport root volume!")
|
|
8317
8491
|
return None
|
|
8318
8492
|
self.logger.debug(
|
|
8319
8493
|
"Transport root volume ID -> %d",
|
|
@@ -8326,7 +8500,7 @@ class OTCS:
|
|
|
8326
8500
|
)
|
|
8327
8501
|
transport_package_volume_id = self.get_result_value(response=response, key="id")
|
|
8328
8502
|
if not transport_package_volume_id:
|
|
8329
|
-
self.logger.error("Failed to retrieve transport package volume")
|
|
8503
|
+
self.logger.error("Failed to retrieve transport package volume!")
|
|
8330
8504
|
return None
|
|
8331
8505
|
self.logger.debug(
|
|
8332
8506
|
"Transport package volume ID -> %d",
|
|
@@ -8345,20 +8519,20 @@ class OTCS:
|
|
|
8345
8519
|
package_id = self.get_result_value(response=response, key="id")
|
|
8346
8520
|
if package_id:
|
|
8347
8521
|
self.logger.debug(
|
|
8348
|
-
"Transport package -> '%s' does already exist; existing package ID -> %d",
|
|
8522
|
+
"Transport package -> '%s' does already exist; existing package ID -> %d.",
|
|
8349
8523
|
package_name,
|
|
8350
8524
|
package_id,
|
|
8351
8525
|
)
|
|
8352
8526
|
else:
|
|
8353
8527
|
self.logger.debug(
|
|
8354
|
-
"Transport package -> '%s' does not yet exist, loading from -> %s",
|
|
8528
|
+
"Transport package -> '%s' does not yet exist, loading from -> '%s'...",
|
|
8355
8529
|
package_name,
|
|
8356
8530
|
package_url,
|
|
8357
8531
|
)
|
|
8358
8532
|
# If we have string replacements configured execute them now:
|
|
8359
8533
|
if replacements:
|
|
8360
8534
|
self.logger.debug(
|
|
8361
|
-
"Transport -> '%s' has replacements -> %s",
|
|
8535
|
+
"Transport -> '%s' has replacements -> %s.",
|
|
8362
8536
|
package_name,
|
|
8363
8537
|
str(replacements),
|
|
8364
8538
|
)
|
|
@@ -8368,13 +8542,13 @@ class OTCS:
|
|
|
8368
8542
|
)
|
|
8369
8543
|
else:
|
|
8370
8544
|
self.logger.debug(
|
|
8371
|
-
"Transport -> '%s' has no replacements
|
|
8545
|
+
"Transport -> '%s' has no replacements.",
|
|
8372
8546
|
package_name,
|
|
8373
8547
|
)
|
|
8374
8548
|
# If we have data extractions configured execute them now:
|
|
8375
8549
|
if extractions:
|
|
8376
8550
|
self.logger.debug(
|
|
8377
|
-
"Transport -> '%s' has extractions -> %s",
|
|
8551
|
+
"Transport -> '%s' has extractions -> %s.",
|
|
8378
8552
|
package_name,
|
|
8379
8553
|
str(extractions),
|
|
8380
8554
|
)
|
|
@@ -8383,7 +8557,7 @@ class OTCS:
|
|
|
8383
8557
|
extractions=extractions,
|
|
8384
8558
|
)
|
|
8385
8559
|
else:
|
|
8386
|
-
self.logger.debug("Transport -> '%s' has no extractions
|
|
8560
|
+
self.logger.debug("Transport -> '%s' has no extractions.", package_name)
|
|
8387
8561
|
|
|
8388
8562
|
# Upload package to Transport Warehouse:
|
|
8389
8563
|
response = self.upload_file_to_volume(
|
|
@@ -8395,7 +8569,7 @@ class OTCS:
|
|
|
8395
8569
|
package_id = self.get_result_value(response=response, key="id")
|
|
8396
8570
|
if not package_id:
|
|
8397
8571
|
self.logger.error(
|
|
8398
|
-
"Failed to upload transport package -> %s",
|
|
8572
|
+
"Failed to upload transport package -> '%s'!",
|
|
8399
8573
|
package_url,
|
|
8400
8574
|
)
|
|
8401
8575
|
return None
|
|
@@ -8438,7 +8612,7 @@ class OTCS:
|
|
|
8438
8612
|
workbench_id = self.get_result_value(response=response, key="id")
|
|
8439
8613
|
if workbench_id:
|
|
8440
8614
|
self.logger.debug(
|
|
8441
|
-
"Workbench -> '%s' does already exist but is not successfully deployed; existing workbench ID -> %d",
|
|
8615
|
+
"Workbench -> '%s' does already exist but is not successfully deployed; existing workbench ID -> %d.",
|
|
8442
8616
|
workbench_name,
|
|
8443
8617
|
workbench_id,
|
|
8444
8618
|
)
|
|
@@ -8454,14 +8628,14 @@ class OTCS:
|
|
|
8454
8628
|
)
|
|
8455
8629
|
return None
|
|
8456
8630
|
self.logger.debug(
|
|
8457
|
-
"Successfully created workbench -> '%s'; new workbench ID -> %d",
|
|
8631
|
+
"Successfully created workbench -> '%s'; new workbench ID -> %d.",
|
|
8458
8632
|
workbench_name,
|
|
8459
8633
|
workbench_id,
|
|
8460
8634
|
)
|
|
8461
8635
|
|
|
8462
8636
|
# Step 3: Unpack Transport Package to Workbench
|
|
8463
8637
|
self.logger.debug(
|
|
8464
|
-
"Unpack transport package -> '%s' (%d) to workbench -> '%s' (%d)",
|
|
8638
|
+
"Unpack transport package -> '%s' (%d) to workbench -> '%s' (%d)...",
|
|
8465
8639
|
package_name,
|
|
8466
8640
|
package_id,
|
|
8467
8641
|
workbench_name,
|
|
@@ -8473,29 +8647,36 @@ class OTCS:
|
|
|
8473
8647
|
)
|
|
8474
8648
|
if not response:
|
|
8475
8649
|
self.logger.error(
|
|
8476
|
-
"Failed to unpack the transport package -> '%s'",
|
|
8650
|
+
"Failed to unpack the transport package -> '%s'!",
|
|
8477
8651
|
package_name,
|
|
8478
8652
|
)
|
|
8479
8653
|
return None
|
|
8480
8654
|
self.logger.debug(
|
|
8481
|
-
"Successfully unpackaged to workbench -> '%s' (%s)",
|
|
8655
|
+
"Successfully unpackaged to workbench -> '%s' (%s).",
|
|
8482
8656
|
workbench_name,
|
|
8483
8657
|
str(workbench_id),
|
|
8484
8658
|
)
|
|
8485
8659
|
|
|
8486
8660
|
# Step 4: Deploy Workbench
|
|
8487
8661
|
self.logger.debug(
|
|
8488
|
-
"Deploy workbench -> '%s' (%s)",
|
|
8662
|
+
"Deploy workbench -> '%s' (%s)...",
|
|
8489
8663
|
workbench_name,
|
|
8490
8664
|
str(workbench_id),
|
|
8491
8665
|
)
|
|
8492
|
-
response = self.deploy_workbench(workbench_id=workbench_id)
|
|
8493
|
-
if not response:
|
|
8494
|
-
self.logger.error(
|
|
8666
|
+
response, errors = self.deploy_workbench(workbench_id=workbench_id)
|
|
8667
|
+
if not response or errors > 0:
|
|
8668
|
+
self.logger.error(
|
|
8669
|
+
"Failed to deploy workbench -> '%s' (%s)!%s",
|
|
8670
|
+
workbench_name,
|
|
8671
|
+
str(workbench_id),
|
|
8672
|
+
" {} error{} occured during deployment.".format(errors, "s" if errors > 1 else "")
|
|
8673
|
+
if errors > 0
|
|
8674
|
+
else "",
|
|
8675
|
+
)
|
|
8495
8676
|
return None
|
|
8496
8677
|
|
|
8497
8678
|
self.logger.debug(
|
|
8498
|
-
"Successfully deployed workbench -> '%s' (%s)",
|
|
8679
|
+
"Successfully deployed workbench -> '%s' (%s).",
|
|
8499
8680
|
workbench_name,
|
|
8500
8681
|
str(workbench_id),
|
|
8501
8682
|
)
|
|
@@ -8589,7 +8770,7 @@ class OTCS:
|
|
|
8589
8770
|
)
|
|
8590
8771
|
continue
|
|
8591
8772
|
self.logger.debug(
|
|
8592
|
-
"Replace -> %s with -> %s in
|
|
8773
|
+
"Replace -> %s with -> %s in transport package -> %s",
|
|
8593
8774
|
replacement["placeholder"],
|
|
8594
8775
|
replacement["value"],
|
|
8595
8776
|
zip_file_folder,
|
|
@@ -8606,21 +8787,21 @@ class OTCS:
|
|
|
8606
8787
|
)
|
|
8607
8788
|
if found:
|
|
8608
8789
|
self.logger.debug(
|
|
8609
|
-
"Replacement -> %s has been completed successfully for
|
|
8790
|
+
"Replacement -> %s has been completed successfully for transport package -> '%s'.",
|
|
8610
8791
|
replacement,
|
|
8611
8792
|
zip_file_folder,
|
|
8612
8793
|
)
|
|
8613
8794
|
modified = True
|
|
8614
8795
|
else:
|
|
8615
8796
|
self.logger.warning(
|
|
8616
|
-
"Replacement -> %s not found in
|
|
8797
|
+
"Replacement -> %s not found in transport package -> '%s'!",
|
|
8617
8798
|
replacement,
|
|
8618
8799
|
zip_file_folder,
|
|
8619
8800
|
)
|
|
8620
8801
|
|
|
8621
8802
|
if not modified:
|
|
8622
8803
|
self.logger.warning(
|
|
8623
|
-
"None of the specified replacements have been found in
|
|
8804
|
+
"None of the specified replacements have been found in transport package -> %s. No need to create a new transport package.",
|
|
8624
8805
|
zip_file_folder,
|
|
8625
8806
|
)
|
|
8626
8807
|
return False
|
|
@@ -8628,7 +8809,7 @@ class OTCS:
|
|
|
8628
8809
|
# Create the new zip file and add all files from the directory to it
|
|
8629
8810
|
new_zip_file_path = os.path.dirname(zip_file_path) + "/new_" + os.path.basename(zip_file_path)
|
|
8630
8811
|
self.logger.debug(
|
|
8631
|
-
"Content of transport -> '%s' has been modified - repacking to new zip file -> %s",
|
|
8812
|
+
"Content of transport -> '%s' has been modified - repacking to new zip file -> '%s'...",
|
|
8632
8813
|
zip_file_folder,
|
|
8633
8814
|
new_zip_file_path,
|
|
8634
8815
|
)
|
|
@@ -8645,13 +8826,13 @@ class OTCS:
|
|
|
8645
8826
|
zip_ref.close()
|
|
8646
8827
|
old_zip_file_path = os.path.dirname(zip_file_path) + "/old_" + os.path.basename(zip_file_path)
|
|
8647
8828
|
self.logger.debug(
|
|
8648
|
-
"Rename orginal transport zip file -> '%s' to -> '%s'",
|
|
8829
|
+
"Rename orginal transport zip file -> '%s' to -> '%s'...",
|
|
8649
8830
|
zip_file_path,
|
|
8650
8831
|
old_zip_file_path,
|
|
8651
8832
|
)
|
|
8652
8833
|
os.rename(zip_file_path, old_zip_file_path)
|
|
8653
8834
|
self.logger.debug(
|
|
8654
|
-
"Rename new transport zip file -> '%s' to -> '%s'",
|
|
8835
|
+
"Rename new transport zip file -> '%s' to -> '%s'...",
|
|
8655
8836
|
new_zip_file_path,
|
|
8656
8837
|
zip_file_path,
|
|
8657
8838
|
)
|
|
@@ -8684,7 +8865,7 @@ class OTCS:
|
|
|
8684
8865
|
"""
|
|
8685
8866
|
|
|
8686
8867
|
if not os.path.isfile(zip_file_path):
|
|
8687
|
-
self.logger.error("Zip file -> '%s' not found
|
|
8868
|
+
self.logger.error("Zip file -> '%s' not found!", zip_file_path)
|
|
8688
8869
|
return False
|
|
8689
8870
|
|
|
8690
8871
|
# Extract the zip file to a temporary directory
|
|
@@ -8696,7 +8877,7 @@ class OTCS:
|
|
|
8696
8877
|
for extraction in extractions:
|
|
8697
8878
|
if "xpath" not in extraction:
|
|
8698
8879
|
self.logger.error(
|
|
8699
|
-
"Extraction needs an
|
|
8880
|
+
"Extraction needs an xpath but it is not specified! Skipping...",
|
|
8700
8881
|
)
|
|
8701
8882
|
continue
|
|
8702
8883
|
# Check if the extraction is explicitly disabled:
|
|
@@ -8709,7 +8890,7 @@ class OTCS:
|
|
|
8709
8890
|
|
|
8710
8891
|
xpath = extraction["xpath"]
|
|
8711
8892
|
self.logger.debug(
|
|
8712
|
-
"Using xpath -> %s to extract the data",
|
|
8893
|
+
"Using xpath -> %s to extract the data.",
|
|
8713
8894
|
xpath,
|
|
8714
8895
|
)
|
|
8715
8896
|
|
|
@@ -8721,7 +8902,7 @@ class OTCS:
|
|
|
8721
8902
|
)
|
|
8722
8903
|
if extracted_data:
|
|
8723
8904
|
self.logger.debug(
|
|
8724
|
-
"Extraction with
|
|
8905
|
+
"Extraction with xpath -> %s has been successfully completed for transport package -> '%s'.",
|
|
8725
8906
|
xpath,
|
|
8726
8907
|
zip_file_folder,
|
|
8727
8908
|
)
|
|
@@ -8729,7 +8910,7 @@ class OTCS:
|
|
|
8729
8910
|
extraction["data"] = extracted_data
|
|
8730
8911
|
else:
|
|
8731
8912
|
self.logger.warning(
|
|
8732
|
-
"Extraction with
|
|
8913
|
+
"Extraction with xpath -> %s has not delivered any data for transport package -> '%s'!",
|
|
8733
8914
|
xpath,
|
|
8734
8915
|
zip_file_folder,
|
|
8735
8916
|
)
|
|
@@ -8752,6 +8933,36 @@ class OTCS:
|
|
|
8752
8933
|
Business Object Types information (for all external systems)
|
|
8753
8934
|
or None if the request fails.
|
|
8754
8935
|
|
|
8936
|
+
Example:
|
|
8937
|
+
{
|
|
8938
|
+
'links': {
|
|
8939
|
+
'data': {
|
|
8940
|
+
'self': {
|
|
8941
|
+
'body': '',
|
|
8942
|
+
'content_type': '',
|
|
8943
|
+
'href': '/api/v2/businessobjecttypes',
|
|
8944
|
+
'method': 'GET',
|
|
8945
|
+
'name': ''
|
|
8946
|
+
}
|
|
8947
|
+
}
|
|
8948
|
+
},
|
|
8949
|
+
'results': [
|
|
8950
|
+
{
|
|
8951
|
+
'data': {
|
|
8952
|
+
'properties': {
|
|
8953
|
+
'bo_type': 'account',
|
|
8954
|
+
'bo_type_id': 54,
|
|
8955
|
+
'bo_type_name': 'gw.account',
|
|
8956
|
+
'ext_system_id': 'Guidewire Policy Center',
|
|
8957
|
+
'is_default_Search': True,
|
|
8958
|
+
'workspace_type_id': 33
|
|
8959
|
+
}
|
|
8960
|
+
}
|
|
8961
|
+
},
|
|
8962
|
+
...
|
|
8963
|
+
]
|
|
8964
|
+
}
|
|
8965
|
+
|
|
8755
8966
|
"""
|
|
8756
8967
|
|
|
8757
8968
|
request_url = self.config()["businessObjectTypesUrl"]
|
|
@@ -9381,6 +9592,7 @@ class OTCS:
|
|
|
9381
9592
|
|
|
9382
9593
|
# end method definition
|
|
9383
9594
|
|
|
9595
|
+
@cache
|
|
9384
9596
|
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="get_workspace_type")
|
|
9385
9597
|
def get_workspace_type(
|
|
9386
9598
|
self,
|
|
@@ -9401,8 +9613,11 @@ class OTCS:
|
|
|
9401
9613
|
Workspace Types or None if the request fails.
|
|
9402
9614
|
|
|
9403
9615
|
Example:
|
|
9404
|
-
|
|
9405
|
-
|
|
9616
|
+
{
|
|
9617
|
+
'icon_url': '/cssupport/otsapxecm/wksp_contract_cust.png',
|
|
9618
|
+
'is_policies_enabled': False,
|
|
9619
|
+
'workspace_type': 'Sales Contract'
|
|
9620
|
+
}
|
|
9406
9621
|
|
|
9407
9622
|
"""
|
|
9408
9623
|
|
|
@@ -9580,7 +9795,7 @@ class OTCS:
|
|
|
9580
9795
|
def get_workspace(
|
|
9581
9796
|
self,
|
|
9582
9797
|
node_id: int,
|
|
9583
|
-
fields:
|
|
9798
|
+
fields: str | list = "properties", # per default we just get the most important information
|
|
9584
9799
|
metadata: bool = False,
|
|
9585
9800
|
) -> dict | None:
|
|
9586
9801
|
"""Get a workspace based on the node ID.
|
|
@@ -9594,11 +9809,11 @@ class OTCS:
|
|
|
9594
9809
|
Possible fields include:
|
|
9595
9810
|
- "properties" (can be further restricted by specifying sub-fields,
|
|
9596
9811
|
e.g., "properties{id,name,parent_id,description}")
|
|
9597
|
-
- "
|
|
9598
|
-
- "
|
|
9599
|
-
|
|
9600
|
-
- "
|
|
9601
|
-
|
|
9812
|
+
- "business_properties" (all the information for the business object and the external system)
|
|
9813
|
+
- "categories" (the category data of the workspace item)
|
|
9814
|
+
- "workspace_references" (a list with the references to business objects in external systems)
|
|
9815
|
+
- "display_urls" (a list with the URLs to external business systems)
|
|
9816
|
+
- "wksp_info" (currently just the icon information of the workspace)
|
|
9602
9817
|
This parameter can be a string to select one field group or a list of
|
|
9603
9818
|
strings to select multiple field groups.
|
|
9604
9819
|
Defaults to "properties".
|
|
@@ -9692,10 +9907,31 @@ class OTCS:
|
|
|
9692
9907
|
'wksp_type_name': 'Vendor',
|
|
9693
9908
|
'xgov_workspace_type': ''
|
|
9694
9909
|
}
|
|
9910
|
+
'display_urls': [
|
|
9911
|
+
{
|
|
9912
|
+
'business_object_type': 'LFA1',
|
|
9913
|
+
'business_object_type_id': 30,
|
|
9914
|
+
'business_object_type_name': 'Customer',
|
|
9915
|
+
'displayUrl': '/sap/bc/gui/sap/its/webgui?~logingroup=SPACE&~transaction=%2fOTX%2fRM_WSC_START_BO+KEY%3dpc%3AS3XljqcFfD0pakDIjUKul%3bOBJTYPE%3daccount&~OkCode=ONLI',
|
|
9916
|
+
'external_system_id': 'TE1',
|
|
9917
|
+
'external_system_name': 'SAP S/4HANA'
|
|
9918
|
+
}
|
|
9919
|
+
]
|
|
9695
9920
|
'wksp_info':
|
|
9696
9921
|
{
|
|
9697
9922
|
'wksp_type_icon': '/appimg/ot_bws/icons/16634%2Esvg?v=161194_13949'
|
|
9698
9923
|
}
|
|
9924
|
+
'workspace_references': [
|
|
9925
|
+
{
|
|
9926
|
+
'business_object_id': '0000010020',
|
|
9927
|
+
'business_object_type': 'LFA1',
|
|
9928
|
+
'business_object_type_id': 30,
|
|
9929
|
+
'external_system_id': 'TE1',
|
|
9930
|
+
'has_default_display': True,
|
|
9931
|
+
'has_default_search': True,
|
|
9932
|
+
'workspace_type_id': 37
|
|
9933
|
+
}
|
|
9934
|
+
]
|
|
9699
9935
|
},
|
|
9700
9936
|
'metadata': {...},
|
|
9701
9937
|
'metadata_order': {
|
|
@@ -9703,20 +9939,6 @@ class OTCS:
|
|
|
9703
9939
|
}
|
|
9704
9940
|
}
|
|
9705
9941
|
],
|
|
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
9942
|
}
|
|
9721
9943
|
```
|
|
9722
9944
|
|
|
@@ -9762,6 +9984,8 @@ class OTCS:
|
|
|
9762
9984
|
expanded_view: bool = True,
|
|
9763
9985
|
page: int | None = None,
|
|
9764
9986
|
limit: int | None = None,
|
|
9987
|
+
fields: str | list = "properties", # per default we just get the most important information
|
|
9988
|
+
metadata: bool = False,
|
|
9765
9989
|
) -> dict | None:
|
|
9766
9990
|
"""Get all workspace instances of a given type.
|
|
9767
9991
|
|
|
@@ -9794,6 +10018,25 @@ class OTCS:
|
|
|
9794
10018
|
The page to be returned (if more workspace instances exist
|
|
9795
10019
|
than given by the page limit).
|
|
9796
10020
|
The default is None.
|
|
10021
|
+
fields (str | list, optional):
|
|
10022
|
+
Which fields to retrieve. This can have a significant
|
|
10023
|
+
impact on performance.
|
|
10024
|
+
Possible fields include:
|
|
10025
|
+
- "properties" (can be further restricted by specifying sub-fields,
|
|
10026
|
+
e.g., "properties{id,name,parent_id,description}")
|
|
10027
|
+
- "categories"
|
|
10028
|
+
- "versions" (can be further restricted by specifying ".element(0)" to
|
|
10029
|
+
retrieve only the latest version)
|
|
10030
|
+
- "permissions" (can be further restricted by specifying ".limit(5)" to
|
|
10031
|
+
retrieve only the first 5 permissions)
|
|
10032
|
+
This parameter can be a string to select one field group or a list of
|
|
10033
|
+
strings to select multiple field groups.
|
|
10034
|
+
Defaults to "properties".
|
|
10035
|
+
metadata (bool, optional):
|
|
10036
|
+
Whether to return metadata (data type, field length, min/max values,...)
|
|
10037
|
+
about the data.
|
|
10038
|
+
Metadata will be returned under `results.metadata`, `metadata_map`,
|
|
10039
|
+
or `metadata_order`.
|
|
9797
10040
|
|
|
9798
10041
|
Returns:
|
|
9799
10042
|
dict | None:
|
|
@@ -9809,6 +10052,8 @@ class OTCS:
|
|
|
9809
10052
|
expanded_view=expanded_view,
|
|
9810
10053
|
page=page,
|
|
9811
10054
|
limit=limit,
|
|
10055
|
+
fields=fields,
|
|
10056
|
+
metadata=metadata,
|
|
9812
10057
|
)
|
|
9813
10058
|
|
|
9814
10059
|
# end method definition
|
|
@@ -9819,6 +10064,9 @@ class OTCS:
|
|
|
9819
10064
|
type_id: int | None = None,
|
|
9820
10065
|
expanded_view: bool = True,
|
|
9821
10066
|
page_size: int = 100,
|
|
10067
|
+
limit: int | None = None,
|
|
10068
|
+
fields: str | list = "properties", # per default we just get the most important information
|
|
10069
|
+
metadata: bool = False,
|
|
9822
10070
|
) -> iter:
|
|
9823
10071
|
"""Get an iterator object to traverse all workspace instances of a workspace type.
|
|
9824
10072
|
|
|
@@ -9849,6 +10097,29 @@ class OTCS:
|
|
|
9849
10097
|
page_size (int | None, optional):
|
|
9850
10098
|
The maximum number of workspace instances that should be delivered in one page.
|
|
9851
10099
|
The default is 100. If None is given then the internal OTCS limit seems to be 500.
|
|
10100
|
+
limit (int | None = None), optional):
|
|
10101
|
+
The maximum number of workspaces to return in total.
|
|
10102
|
+
If None (default) all workspaces are returned.
|
|
10103
|
+
If a number is provided only up to this number of results is returned.
|
|
10104
|
+
fields (str | list, optional):
|
|
10105
|
+
Which fields to retrieve. This can have a significant
|
|
10106
|
+
impact on performance.
|
|
10107
|
+
Possible fields include:
|
|
10108
|
+
- "properties" (can be further restricted by specifying sub-fields,
|
|
10109
|
+
e.g., "properties{id,name,parent_id,description}")
|
|
10110
|
+
- "categories"
|
|
10111
|
+
- "versions" (can be further restricted by specifying ".element(0)" to
|
|
10112
|
+
retrieve only the latest version)
|
|
10113
|
+
- "permissions" (can be further restricted by specifying ".limit(5)" to
|
|
10114
|
+
retrieve only the first 5 permissions)
|
|
10115
|
+
This parameter can be a string to select one field group or a list of
|
|
10116
|
+
strings to select multiple field groups.
|
|
10117
|
+
Defaults to "properties".
|
|
10118
|
+
metadata (bool, optional):
|
|
10119
|
+
Whether to return metadata (data type, field length, min/max values,...)
|
|
10120
|
+
about the data.
|
|
10121
|
+
Metadata will be returned under `results.metadata`, `metadata_map`,
|
|
10122
|
+
or `metadata_order`.
|
|
9852
10123
|
|
|
9853
10124
|
Returns:
|
|
9854
10125
|
iter:
|
|
@@ -9863,8 +10134,10 @@ class OTCS:
|
|
|
9863
10134
|
type_id=type_id,
|
|
9864
10135
|
name="",
|
|
9865
10136
|
expanded_view=expanded_view,
|
|
9866
|
-
page=1,
|
|
9867
10137
|
limit=1,
|
|
10138
|
+
page=1,
|
|
10139
|
+
fields=fields,
|
|
10140
|
+
metadata=metadata,
|
|
9868
10141
|
)
|
|
9869
10142
|
if not response or "results" not in response:
|
|
9870
10143
|
# Don't return None! Plain return is what we need for iterators.
|
|
@@ -9873,9 +10146,12 @@ class OTCS:
|
|
|
9873
10146
|
return
|
|
9874
10147
|
|
|
9875
10148
|
number_of_instances = response["paging"]["total_count"]
|
|
10149
|
+
if limit and number_of_instances > limit:
|
|
10150
|
+
number_of_instances = limit
|
|
10151
|
+
|
|
9876
10152
|
if not number_of_instances:
|
|
9877
10153
|
self.logger.debug(
|
|
9878
|
-
"Workspace type -> %s does not have instances! Cannot iterate over instances.",
|
|
10154
|
+
"Workspace type -> '%s' does not have instances! Cannot iterate over instances.",
|
|
9879
10155
|
type_name if type_name else str(type_id),
|
|
9880
10156
|
)
|
|
9881
10157
|
# Don't return None! Plain return is what we need for iterators.
|
|
@@ -9897,7 +10173,9 @@ class OTCS:
|
|
|
9897
10173
|
name="",
|
|
9898
10174
|
expanded_view=expanded_view,
|
|
9899
10175
|
page=page,
|
|
9900
|
-
limit=page_size,
|
|
10176
|
+
limit=page_size if limit is None else limit,
|
|
10177
|
+
fields=fields,
|
|
10178
|
+
metadata=metadata,
|
|
9901
10179
|
)
|
|
9902
10180
|
if not response or not response.get("results", None):
|
|
9903
10181
|
self.logger.warning(
|
|
@@ -9923,9 +10201,9 @@ class OTCS:
|
|
|
9923
10201
|
expanded_view: bool = True,
|
|
9924
10202
|
limit: int | None = None,
|
|
9925
10203
|
page: int | None = None,
|
|
9926
|
-
fields:
|
|
10204
|
+
fields: str | list = "properties", # per default we just get the most important information
|
|
9927
10205
|
metadata: bool = False,
|
|
9928
|
-
timeout:
|
|
10206
|
+
timeout: float = REQUEST_TIMEOUT,
|
|
9929
10207
|
) -> dict | None:
|
|
9930
10208
|
"""Lookup workspaces based on workspace type and workspace name.
|
|
9931
10209
|
|
|
@@ -9981,7 +10259,7 @@ class OTCS:
|
|
|
9981
10259
|
about the data.
|
|
9982
10260
|
Metadata will be returned under `results.metadata`, `metadata_map`,
|
|
9983
10261
|
or `metadata_order`.
|
|
9984
|
-
timeout (
|
|
10262
|
+
timeout (float, optional):
|
|
9985
10263
|
Specific timeout for the request in seconds. The default is the standard
|
|
9986
10264
|
timeout value REQUEST_TIMEOUT used by the OTCS module.
|
|
9987
10265
|
|
|
@@ -10191,7 +10469,7 @@ class OTCS:
|
|
|
10191
10469
|
external_system_name: str,
|
|
10192
10470
|
business_object_type: str,
|
|
10193
10471
|
business_object_id: str,
|
|
10194
|
-
|
|
10472
|
+
metadata: bool = False,
|
|
10195
10473
|
show_error: bool = False,
|
|
10196
10474
|
) -> dict | None:
|
|
10197
10475
|
"""Get a workspace based on the business object of an external system.
|
|
@@ -10203,7 +10481,7 @@ class OTCS:
|
|
|
10203
10481
|
Type of the Business object, e.g. KNA1 for SAP customers
|
|
10204
10482
|
business_object_id (str):
|
|
10205
10483
|
ID of the business object in the external system
|
|
10206
|
-
|
|
10484
|
+
metadata (bool, optional):
|
|
10207
10485
|
Whether or not workspace metadata (categories) should be returned.
|
|
10208
10486
|
Default is False.
|
|
10209
10487
|
show_error (bool, optional):
|
|
@@ -10285,7 +10563,7 @@ class OTCS:
|
|
|
10285
10563
|
+ "/boids/"
|
|
10286
10564
|
+ business_object_id
|
|
10287
10565
|
)
|
|
10288
|
-
if
|
|
10566
|
+
if metadata:
|
|
10289
10567
|
request_url += "?metadata"
|
|
10290
10568
|
|
|
10291
10569
|
request_header = self.request_form_header()
|
|
@@ -10319,7 +10597,7 @@ class OTCS:
|
|
|
10319
10597
|
# end method definition
|
|
10320
10598
|
|
|
10321
10599
|
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="lookup_workspace")
|
|
10322
|
-
def
|
|
10600
|
+
def lookup_workspaces(
|
|
10323
10601
|
self,
|
|
10324
10602
|
type_name: str,
|
|
10325
10603
|
category: str,
|
|
@@ -10327,11 +10605,12 @@ class OTCS:
|
|
|
10327
10605
|
value: str,
|
|
10328
10606
|
attribute_set: str | None = None,
|
|
10329
10607
|
) -> dict | None:
|
|
10330
|
-
"""Lookup
|
|
10608
|
+
"""Lookup workspaces that have a specified value in a category attribute.
|
|
10331
10609
|
|
|
10332
10610
|
Args:
|
|
10333
10611
|
type_name (str):
|
|
10334
|
-
The name of the workspace type.
|
|
10612
|
+
The name of the workspace type. This is required to determine
|
|
10613
|
+
the parent folder in which the workspaces of this type reside.
|
|
10335
10614
|
category (str):
|
|
10336
10615
|
The name of the category.
|
|
10337
10616
|
attribute (str):
|
|
@@ -10339,7 +10618,8 @@ class OTCS:
|
|
|
10339
10618
|
value (str):
|
|
10340
10619
|
The lookup value that is matched agains the node attribute value.
|
|
10341
10620
|
attribute_set (str | None, optional):
|
|
10342
|
-
The name of the attribute set
|
|
10621
|
+
The name of the attribute set. If None (default) the attribute to lookup
|
|
10622
|
+
is supposed to be a top-level attribute.
|
|
10343
10623
|
|
|
10344
10624
|
Returns:
|
|
10345
10625
|
dict | None:
|
|
@@ -10358,12 +10638,44 @@ class OTCS:
|
|
|
10358
10638
|
)
|
|
10359
10639
|
return None
|
|
10360
10640
|
|
|
10361
|
-
return self.
|
|
10641
|
+
return self.lookup_nodes(
|
|
10362
10642
|
parent_node_id=parent_id, category=category, attribute=attribute, value=value, attribute_set=attribute_set
|
|
10363
10643
|
)
|
|
10364
10644
|
|
|
10365
10645
|
# end method definition
|
|
10366
10646
|
|
|
10647
|
+
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="get_workspace")
|
|
10648
|
+
def get_workspace_references(
|
|
10649
|
+
self,
|
|
10650
|
+
node_id: int,
|
|
10651
|
+
) -> list | None:
|
|
10652
|
+
"""Get a workspace rewferences to business objects in external systems.
|
|
10653
|
+
|
|
10654
|
+
Args:
|
|
10655
|
+
node_id (int):
|
|
10656
|
+
The node ID of the workspace to retrieve.
|
|
10657
|
+
|
|
10658
|
+
Returns:
|
|
10659
|
+
list | None:
|
|
10660
|
+
A List of references to business objects in external systems.
|
|
10661
|
+
|
|
10662
|
+
"""
|
|
10663
|
+
|
|
10664
|
+
response = self.get_workspace(node_id=node_id, fields="workspace_references")
|
|
10665
|
+
|
|
10666
|
+
results = response.get("results")
|
|
10667
|
+
if not results:
|
|
10668
|
+
return None
|
|
10669
|
+
data = results.get("data")
|
|
10670
|
+
if not data:
|
|
10671
|
+
return None
|
|
10672
|
+
|
|
10673
|
+
workspace_references: list = data.get("workspace_references")
|
|
10674
|
+
|
|
10675
|
+
return workspace_references
|
|
10676
|
+
|
|
10677
|
+
# end method definition
|
|
10678
|
+
|
|
10367
10679
|
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="set_workspace_reference")
|
|
10368
10680
|
def set_workspace_reference(
|
|
10369
10681
|
self,
|
|
@@ -10422,13 +10734,88 @@ class OTCS:
|
|
|
10422
10734
|
headers=request_header,
|
|
10423
10735
|
data=workspace_put_data,
|
|
10424
10736
|
timeout=None,
|
|
10425
|
-
warning_message="Cannot update reference for workspace ID -> {} with business object connection -> ({}, {}, {})".format(
|
|
10737
|
+
warning_message="Cannot update reference for workspace ID -> {} with business object connection -> ('{}', '{}', {})".format(
|
|
10426
10738
|
workspace_id,
|
|
10427
10739
|
external_system_id,
|
|
10428
10740
|
bo_type,
|
|
10429
10741
|
bo_id,
|
|
10430
10742
|
),
|
|
10431
|
-
failure_message="Failed to update reference for workspace ID -> {} with business object connection -> ({}, {}, {})".format(
|
|
10743
|
+
failure_message="Failed to update reference for workspace ID -> {} with business object connection -> ('{}', '{}', {})".format(
|
|
10744
|
+
workspace_id,
|
|
10745
|
+
external_system_id,
|
|
10746
|
+
bo_type,
|
|
10747
|
+
bo_id,
|
|
10748
|
+
),
|
|
10749
|
+
show_error=show_error,
|
|
10750
|
+
)
|
|
10751
|
+
|
|
10752
|
+
# end method definition
|
|
10753
|
+
|
|
10754
|
+
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="delete_workspace_reference")
|
|
10755
|
+
def delete_workspace_reference(
|
|
10756
|
+
self,
|
|
10757
|
+
workspace_id: int,
|
|
10758
|
+
external_system_id: str | None = None,
|
|
10759
|
+
bo_type: str | None = None,
|
|
10760
|
+
bo_id: str | None = None,
|
|
10761
|
+
show_error: bool = True,
|
|
10762
|
+
) -> dict | None:
|
|
10763
|
+
"""Delete reference of workspace to a business object in an external system.
|
|
10764
|
+
|
|
10765
|
+
Args:
|
|
10766
|
+
workspace_id (int):
|
|
10767
|
+
The ID of the workspace.
|
|
10768
|
+
external_system_id (str | None, optional):
|
|
10769
|
+
Identifier of the external system (None if no external system).
|
|
10770
|
+
bo_type (str | None, optional):
|
|
10771
|
+
Business object type (None if no external system)
|
|
10772
|
+
bo_id (str | None, optional):
|
|
10773
|
+
Business object identifier / key (None if no external system)
|
|
10774
|
+
show_error (bool, optional):
|
|
10775
|
+
Log an error if workspace cration fails. Otherwise log a warning.
|
|
10776
|
+
|
|
10777
|
+
Returns:
|
|
10778
|
+
Request response or None in case of an error.
|
|
10779
|
+
|
|
10780
|
+
"""
|
|
10781
|
+
|
|
10782
|
+
request_url = self.config()["businessWorkspacesUrl"] + "/" + str(workspace_id) + "/workspacereferences"
|
|
10783
|
+
request_header = self.request_form_header()
|
|
10784
|
+
|
|
10785
|
+
if not external_system_id or not bo_type or not bo_id:
|
|
10786
|
+
self.logger.error(
|
|
10787
|
+
"Cannot update workspace reference - required Business Object information is missing!",
|
|
10788
|
+
)
|
|
10789
|
+
return None
|
|
10790
|
+
|
|
10791
|
+
self.logger.debug(
|
|
10792
|
+
"Delete workspace reference of workspace ID -> %d with business object connection -> (%s, %s, %s); calling -> %s",
|
|
10793
|
+
workspace_id,
|
|
10794
|
+
external_system_id,
|
|
10795
|
+
bo_type,
|
|
10796
|
+
bo_id,
|
|
10797
|
+
request_url,
|
|
10798
|
+
)
|
|
10799
|
+
|
|
10800
|
+
workspace_put_data = {
|
|
10801
|
+
"ext_system_id": external_system_id,
|
|
10802
|
+
"bo_type": bo_type,
|
|
10803
|
+
"bo_id": bo_id,
|
|
10804
|
+
}
|
|
10805
|
+
|
|
10806
|
+
return self.do_request(
|
|
10807
|
+
url=request_url,
|
|
10808
|
+
method="DELETE",
|
|
10809
|
+
headers=request_header,
|
|
10810
|
+
data=workspace_put_data,
|
|
10811
|
+
timeout=None,
|
|
10812
|
+
warning_message="Cannot delete reference for workspace ID -> {} with business object connection -> ({}, {}, {})".format(
|
|
10813
|
+
workspace_id,
|
|
10814
|
+
external_system_id,
|
|
10815
|
+
bo_type,
|
|
10816
|
+
bo_id,
|
|
10817
|
+
),
|
|
10818
|
+
failure_message="Failed to delete reference for workspace ID -> {} with business object connection -> ({}, {}, {})".format(
|
|
10432
10819
|
workspace_id,
|
|
10433
10820
|
external_system_id,
|
|
10434
10821
|
bo_type,
|
|
@@ -10475,26 +10862,26 @@ class OTCS:
|
|
|
10475
10862
|
A description of the new workspace.
|
|
10476
10863
|
workspace_type (int):
|
|
10477
10864
|
Type ID of the workspace, indicating its category or function.
|
|
10478
|
-
category_data (dict, optional):
|
|
10865
|
+
category_data (dict | None, optional):
|
|
10479
10866
|
Category and attribute data for the workspace.
|
|
10480
|
-
classifications (list):
|
|
10867
|
+
classifications (list | None, optional):
|
|
10481
10868
|
List of classification item IDs to apply to the new item.
|
|
10482
|
-
external_system_id (str, optional):
|
|
10869
|
+
external_system_id (str | None, optional):
|
|
10483
10870
|
External system identifier if linking the workspace to an external system.
|
|
10484
|
-
bo_type (str, optional):
|
|
10871
|
+
bo_type (str | None, optional):
|
|
10485
10872
|
Business object type, used if linking to an external system.
|
|
10486
|
-
bo_id (str, optional):
|
|
10873
|
+
bo_id (str | None, optional):
|
|
10487
10874
|
Business object identifier or key, used if linking to an external system.
|
|
10488
|
-
parent_id (int, optional):
|
|
10875
|
+
parent_id (int | None, optional):
|
|
10489
10876
|
ID of the parent workspace, required in special cases such as
|
|
10490
10877
|
sub-workspaces or location ambiguity.
|
|
10491
|
-
ibo_workspace_id (int, optional):
|
|
10878
|
+
ibo_workspace_id (int | None, optional):
|
|
10492
10879
|
ID of an existing workspace that is already linked to an external system.
|
|
10493
10880
|
Allows connecting multiple business objects (IBO).
|
|
10494
|
-
external_create_date (str, optional):
|
|
10881
|
+
external_create_date (str | None, optional):
|
|
10495
10882
|
Date of creation in the external system (format: YYYY-MM-DD).
|
|
10496
10883
|
None is the default.
|
|
10497
|
-
external_modify_date (str, optional):
|
|
10884
|
+
external_modify_date (str | None, optional):
|
|
10498
10885
|
Date of last modification in the external system (format: YYYY-MM-DD).
|
|
10499
10886
|
None is the default.
|
|
10500
10887
|
show_error (bool, optional):
|
|
@@ -10670,16 +11057,16 @@ class OTCS:
|
|
|
10670
11057
|
category_data (dict | None, optional):
|
|
10671
11058
|
Category and attribute data.
|
|
10672
11059
|
Default is None (attributes remain unchanged).
|
|
10673
|
-
external_system_id (str, optional):
|
|
11060
|
+
external_system_id (str | None, optional):
|
|
10674
11061
|
Identifier of the external system (None if no external system)
|
|
10675
|
-
bo_type (str, optional):
|
|
11062
|
+
bo_type (str | None, optional):
|
|
10676
11063
|
Business object type (None if no external system)
|
|
10677
|
-
bo_id (str, optional):
|
|
11064
|
+
bo_id (str | None, optional):
|
|
10678
11065
|
Business object identifier / key (None if no external system)
|
|
10679
|
-
external_create_date (str, optional):
|
|
11066
|
+
external_create_date (str | None, optional):
|
|
10680
11067
|
Date of creation in the external system
|
|
10681
11068
|
(format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS).
|
|
10682
|
-
external_modify_date (str, optional):
|
|
11069
|
+
external_modify_date (str | None, optional):
|
|
10683
11070
|
Date of last modification in the external system
|
|
10684
11071
|
(format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS).
|
|
10685
11072
|
show_error (bool, optional):
|
|
@@ -10787,12 +11174,13 @@ class OTCS:
|
|
|
10787
11174
|
def get_workspace_relationships(
|
|
10788
11175
|
self,
|
|
10789
11176
|
workspace_id: int,
|
|
10790
|
-
relationship_type: str | list
|
|
11177
|
+
relationship_type: str | list = "child",
|
|
10791
11178
|
related_workspace_name: str | None = None,
|
|
10792
11179
|
related_workspace_type_id: int | list | None = None,
|
|
10793
11180
|
limit: int | None = None,
|
|
10794
11181
|
page: int | None = None,
|
|
10795
|
-
fields:
|
|
11182
|
+
fields: str | list = "properties", # per default we just get the most important information
|
|
11183
|
+
metadata: bool = False,
|
|
10796
11184
|
) -> dict | None:
|
|
10797
11185
|
"""Get the Workspace relationships to other workspaces.
|
|
10798
11186
|
|
|
@@ -10804,7 +11192,7 @@ class OTCS:
|
|
|
10804
11192
|
workspace_id (int):
|
|
10805
11193
|
The ID of the workspace.
|
|
10806
11194
|
relationship_type (str | list, optional):
|
|
10807
|
-
Either "parent" or "child" (
|
|
11195
|
+
Either "parent" or "child" ("child" is the default).
|
|
10808
11196
|
If both ("child" and "parent") are requested then use a
|
|
10809
11197
|
list like ["child", "parent"].
|
|
10810
11198
|
related_workspace_name (str | None, optional):
|
|
@@ -10834,6 +11222,9 @@ class OTCS:
|
|
|
10834
11222
|
This parameter can be a string to select one field group or a list of
|
|
10835
11223
|
strings to select multiple field groups.
|
|
10836
11224
|
Defaults to "properties".
|
|
11225
|
+
metadata (bool, optional):
|
|
11226
|
+
Whether or not workspace metadata (categories) should be returned.
|
|
11227
|
+
Default is False.
|
|
10837
11228
|
|
|
10838
11229
|
Returns:
|
|
10839
11230
|
dict | None:
|
|
@@ -10928,7 +11319,13 @@ class OTCS:
|
|
|
10928
11319
|
if isinstance(relationship_type, str):
|
|
10929
11320
|
query["where_relationtype"] = relationship_type
|
|
10930
11321
|
elif isinstance(relationship_type, list):
|
|
10931
|
-
|
|
11322
|
+
if any(rt not in ["parent", "child"] for rt in relationship_type):
|
|
11323
|
+
self.logger.error(
|
|
11324
|
+
"Illegal relationship type for related workspace type! Must be either 'parent' or 'child'. -> %s",
|
|
11325
|
+
relationship_type,
|
|
11326
|
+
)
|
|
11327
|
+
return None
|
|
11328
|
+
query["where_rel_types"] = "{'" + ("','").join(relationship_type) + "'}"
|
|
10932
11329
|
else:
|
|
10933
11330
|
self.logger.error(
|
|
10934
11331
|
"Illegal relationship type for related workspace type!",
|
|
@@ -10956,6 +11353,8 @@ class OTCS:
|
|
|
10956
11353
|
|
|
10957
11354
|
encoded_query = urllib.parse.urlencode(query=query, doseq=False)
|
|
10958
11355
|
request_url += "?{}".format(encoded_query)
|
|
11356
|
+
if metadata:
|
|
11357
|
+
request_url += "&metadata"
|
|
10959
11358
|
|
|
10960
11359
|
request_header = self.request_form_header()
|
|
10961
11360
|
|
|
@@ -10980,11 +11379,13 @@ class OTCS:
|
|
|
10980
11379
|
def get_workspace_relationships_iterator(
|
|
10981
11380
|
self,
|
|
10982
11381
|
workspace_id: int,
|
|
10983
|
-
relationship_type: str | list
|
|
11382
|
+
relationship_type: str | list = "child",
|
|
10984
11383
|
related_workspace_name: str | None = None,
|
|
10985
11384
|
related_workspace_type_id: int | list | None = None,
|
|
10986
|
-
fields:
|
|
11385
|
+
fields: str | list = "properties", # per default we just get the most important information
|
|
10987
11386
|
page_size: int = 100,
|
|
11387
|
+
limit: int | None = None,
|
|
11388
|
+
metadata: bool = False,
|
|
10988
11389
|
) -> iter:
|
|
10989
11390
|
"""Get an iterator object to traverse all related workspaces for a workspace.
|
|
10990
11391
|
|
|
@@ -11005,7 +11406,7 @@ class OTCS:
|
|
|
11005
11406
|
workspace_id (int):
|
|
11006
11407
|
The ID of the workspace.
|
|
11007
11408
|
relationship_type (str | list, optional):
|
|
11008
|
-
Either "parent" or "child" (
|
|
11409
|
+
Either "parent" or "child" ("child" is the default).
|
|
11009
11410
|
If both ("child" and "parent") are requested then use a
|
|
11010
11411
|
list like ["child", "parent"].
|
|
11011
11412
|
related_workspace_name (str | None, optional):
|
|
@@ -11015,14 +11416,12 @@ class OTCS:
|
|
|
11015
11416
|
fields (str | list, optional):
|
|
11016
11417
|
Which fields to retrieve. This can have a significant
|
|
11017
11418
|
impact on performance.
|
|
11018
|
-
Possible fields include:
|
|
11419
|
+
Possible fields include (NOTE: "categories" is not supported in this method!!):
|
|
11019
11420
|
- "properties" (can be further restricted by specifying sub-fields,
|
|
11020
11421
|
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)
|
|
11422
|
+
- "business_properties" (all the information for the business object and the external system)
|
|
11423
|
+
- "workspace_references" (a list with the references to business objects in external systems)
|
|
11424
|
+
- "wksp_info" (currently just the icon information of the workspace)
|
|
11026
11425
|
This parameter can be a string to select one field group or a list of
|
|
11027
11426
|
strings to select multiple field groups.
|
|
11028
11427
|
Defaults to "properties".
|
|
@@ -11032,6 +11431,14 @@ class OTCS:
|
|
|
11032
11431
|
The default is None, in this case the internal OTCS limit seems
|
|
11033
11432
|
to be 500.
|
|
11034
11433
|
This is basically the chunk size for the iterator.
|
|
11434
|
+
limit (int | None = None), optional):
|
|
11435
|
+
The maximum number of workspaces to return in total.
|
|
11436
|
+
If None (default) all workspaces are returned.
|
|
11437
|
+
If a number is provided only up to this number of results is returned.
|
|
11438
|
+
metadata (bool, optional):
|
|
11439
|
+
Whether or not workspace metadata should be returned. These are
|
|
11440
|
+
the system level metadata - not the categories of the workspace!
|
|
11441
|
+
Default is False.
|
|
11035
11442
|
|
|
11036
11443
|
Returns:
|
|
11037
11444
|
iter:
|
|
@@ -11049,6 +11456,7 @@ class OTCS:
|
|
|
11049
11456
|
limit=1,
|
|
11050
11457
|
page=1,
|
|
11051
11458
|
fields=fields,
|
|
11459
|
+
metadata=metadata,
|
|
11052
11460
|
)
|
|
11053
11461
|
if not response or "results" not in response:
|
|
11054
11462
|
# Don't return None! Plain return is what we need for iterators.
|
|
@@ -11057,6 +11465,9 @@ class OTCS:
|
|
|
11057
11465
|
return
|
|
11058
11466
|
|
|
11059
11467
|
number_of_related_workspaces = response["paging"]["total_count"]
|
|
11468
|
+
if limit and number_of_related_workspaces > limit:
|
|
11469
|
+
number_of_related_workspaces = limit
|
|
11470
|
+
|
|
11060
11471
|
if not number_of_related_workspaces:
|
|
11061
11472
|
self.logger.debug(
|
|
11062
11473
|
"Workspace with node ID -> %d does not have related workspaces! Cannot iterate over related workspaces.",
|
|
@@ -11080,9 +11491,10 @@ class OTCS:
|
|
|
11080
11491
|
relationship_type=relationship_type,
|
|
11081
11492
|
related_workspace_name=related_workspace_name,
|
|
11082
11493
|
related_workspace_type_id=related_workspace_type_id,
|
|
11083
|
-
limit=page_size,
|
|
11494
|
+
limit=page_size if limit is None else limit,
|
|
11084
11495
|
page=page,
|
|
11085
11496
|
fields=fields,
|
|
11497
|
+
metadata=metadata,
|
|
11086
11498
|
)
|
|
11087
11499
|
if not response or not response.get("results", None):
|
|
11088
11500
|
self.logger.warning(
|
|
@@ -11314,6 +11726,46 @@ class OTCS:
|
|
|
11314
11726
|
dict | None:
|
|
11315
11727
|
Workspace Role Membership or None if the request fails.
|
|
11316
11728
|
|
|
11729
|
+
Example:
|
|
11730
|
+
{
|
|
11731
|
+
'links': {
|
|
11732
|
+
'data': {
|
|
11733
|
+
'self': {
|
|
11734
|
+
'body': '',
|
|
11735
|
+
'content_type': '',
|
|
11736
|
+
'href': '/api/v2/businessworkspaces/80998/roles/81001/members',
|
|
11737
|
+
'method': 'POST',
|
|
11738
|
+
'name': ''
|
|
11739
|
+
}
|
|
11740
|
+
},
|
|
11741
|
+
'results': {
|
|
11742
|
+
'data': {
|
|
11743
|
+
'properties': {
|
|
11744
|
+
'birth_date': None,
|
|
11745
|
+
'business_email': 'lwhite@terrarium.cloud',
|
|
11746
|
+
'business_fax': None,
|
|
11747
|
+
'business_phone': '+1 (345) 4626-333',
|
|
11748
|
+
'cell_phone': None,
|
|
11749
|
+
'deleted': False,
|
|
11750
|
+
'display_language': None,
|
|
11751
|
+
'display_name': 'Liz White',
|
|
11752
|
+
'first_name': 'Liz',
|
|
11753
|
+
'gender': None,
|
|
11754
|
+
'group_id': 16178,
|
|
11755
|
+
'group_name': 'Executive Leadership Team',
|
|
11756
|
+
'home_address_1': None,
|
|
11757
|
+
'home_address_2': None,
|
|
11758
|
+
'home_fax': None,
|
|
11759
|
+
'home_phone': None,
|
|
11760
|
+
'id': 15520,
|
|
11761
|
+
'initials': 'LW',
|
|
11762
|
+
'last_name': 'White',
|
|
11763
|
+
...
|
|
11764
|
+
}
|
|
11765
|
+
}
|
|
11766
|
+
}
|
|
11767
|
+
}
|
|
11768
|
+
|
|
11317
11769
|
"""
|
|
11318
11770
|
|
|
11319
11771
|
self.logger.debug(
|
|
@@ -12022,7 +12474,7 @@ class OTCS:
|
|
|
12022
12474
|
|
|
12023
12475
|
Args:
|
|
12024
12476
|
parent_id (int):
|
|
12025
|
-
The node the
|
|
12477
|
+
The parent node of the new node to create.
|
|
12026
12478
|
subtype (int, optional):
|
|
12027
12479
|
The subtype of the new node. Default is document.
|
|
12028
12480
|
category_ids (int | list[int], optional):
|
|
@@ -12030,7 +12482,7 @@ class OTCS:
|
|
|
12030
12482
|
|
|
12031
12483
|
Returns:
|
|
12032
12484
|
dict | None:
|
|
12033
|
-
|
|
12485
|
+
Node create form data or None if the request fails.
|
|
12034
12486
|
|
|
12035
12487
|
"""
|
|
12036
12488
|
|
|
@@ -12065,6 +12517,151 @@ class OTCS:
|
|
|
12065
12517
|
|
|
12066
12518
|
# end method definition
|
|
12067
12519
|
|
|
12520
|
+
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="get_node_category_update_form")
|
|
12521
|
+
def get_node_category_form(
|
|
12522
|
+
self,
|
|
12523
|
+
node_id: int,
|
|
12524
|
+
category_id: int | None = None,
|
|
12525
|
+
operation: str = "update",
|
|
12526
|
+
) -> dict | None:
|
|
12527
|
+
"""Get the node category update form.
|
|
12528
|
+
|
|
12529
|
+
Args:
|
|
12530
|
+
node_id (int):
|
|
12531
|
+
The ID of the node to update.
|
|
12532
|
+
category_id (int | None, optional):
|
|
12533
|
+
The ID of the category to update.
|
|
12534
|
+
operation (str, optional):
|
|
12535
|
+
The operation to perform. Default is "update". Other possible value is "create".
|
|
12536
|
+
|
|
12537
|
+
Returns:
|
|
12538
|
+
dict | None:
|
|
12539
|
+
Workspace Category Update Form data or None if the request fails.
|
|
12540
|
+
|
|
12541
|
+
Example:
|
|
12542
|
+
{
|
|
12543
|
+
'forms': [
|
|
12544
|
+
{
|
|
12545
|
+
'data': {
|
|
12546
|
+
'20581_1': {'metadata_token': ''},
|
|
12547
|
+
'20581_10': None,
|
|
12548
|
+
'20581_11': None,
|
|
12549
|
+
'20581_12': None,
|
|
12550
|
+
'20581_13': None,
|
|
12551
|
+
'20581_14': [
|
|
12552
|
+
{
|
|
12553
|
+
'20581_14_x_15': None,
|
|
12554
|
+
'20581_14_x_16': None,
|
|
12555
|
+
'20581_14_x_17': None,
|
|
12556
|
+
'20581_14_x_18': None,
|
|
12557
|
+
'20581_14_x_19': None,
|
|
12558
|
+
'20581_14_x_20': None,
|
|
12559
|
+
'20581_14_x_21': None,
|
|
12560
|
+
'20581_14_x_22': None
|
|
12561
|
+
}
|
|
12562
|
+
],
|
|
12563
|
+
'20581_14_1': None,
|
|
12564
|
+
'20581_2': None,
|
|
12565
|
+
'20581_23': [
|
|
12566
|
+
{
|
|
12567
|
+
'20581_23_x_25': None,
|
|
12568
|
+
'20581_23_x_26': None,
|
|
12569
|
+
'20581_23_x_27': None,
|
|
12570
|
+
'20581_23_x_28': None,
|
|
12571
|
+
'20581_23_x_29': None,
|
|
12572
|
+
'20581_23_x_30': None,
|
|
12573
|
+
'20581_23_x_31': None,
|
|
12574
|
+
'20581_23_x_32': None,
|
|
12575
|
+
'20581_23_x_37': None
|
|
12576
|
+
}
|
|
12577
|
+
],
|
|
12578
|
+
'20581_23_1': None,
|
|
12579
|
+
'20581_3': None,
|
|
12580
|
+
'20581_33': {
|
|
12581
|
+
'20581_33_1_34': None,
|
|
12582
|
+
'20581_33_1_35': None,
|
|
12583
|
+
'20581_33_1_36': None
|
|
12584
|
+
},
|
|
12585
|
+
'20581_4': None,
|
|
12586
|
+
'20581_5': None,
|
|
12587
|
+
'20581_6': None,
|
|
12588
|
+
'20581_7': None,
|
|
12589
|
+
'20581_8': None,
|
|
12590
|
+
'20581_9': None
|
|
12591
|
+
},
|
|
12592
|
+
'options': {
|
|
12593
|
+
'fields': {...},
|
|
12594
|
+
'form': {...}
|
|
12595
|
+
},
|
|
12596
|
+
'schema': {
|
|
12597
|
+
'properties': {
|
|
12598
|
+
'20581_1': {
|
|
12599
|
+
'readonly': True,
|
|
12600
|
+
'required': False, 'type': 'object'},
|
|
12601
|
+
'20581_2': {
|
|
12602
|
+
'maxLength': 20,
|
|
12603
|
+
'multilingual': None,
|
|
12604
|
+
'readonly': False,
|
|
12605
|
+
'required': False,
|
|
12606
|
+
'title': 'Order Number',
|
|
12607
|
+
'type': 'string'
|
|
12608
|
+
},
|
|
12609
|
+
'20581_11': {
|
|
12610
|
+
'maxLength': 25,
|
|
12611
|
+
'multilingual': None,
|
|
12612
|
+
'readonly': False,
|
|
12613
|
+
'required': False,
|
|
12614
|
+
'title': 'Order Type',
|
|
12615
|
+
'type': 'string'
|
|
12616
|
+
},
|
|
12617
|
+
... (more fields) ...
|
|
12618
|
+
},
|
|
12619
|
+
'type': 'object'
|
|
12620
|
+
}
|
|
12621
|
+
}
|
|
12622
|
+
]
|
|
12623
|
+
}
|
|
12624
|
+
|
|
12625
|
+
"""
|
|
12626
|
+
|
|
12627
|
+
request_header = self.request_form_header()
|
|
12628
|
+
|
|
12629
|
+
# If no category ID is provided get the current category IDs of the node and take the first one.
|
|
12630
|
+
# TODO: we need to be more clever here if multiple categories are assigned to a node.
|
|
12631
|
+
if category_id is None:
|
|
12632
|
+
category_ids = self.get_node_category_ids(node_id=node_id)
|
|
12633
|
+
if not category_ids or not isinstance(category_ids, list):
|
|
12634
|
+
self.logger.error("Cannot get category IDs for node with ID -> %s", str(node_id))
|
|
12635
|
+
return None
|
|
12636
|
+
category_id = category_ids[0]
|
|
12637
|
+
|
|
12638
|
+
self.logger.debug(
|
|
12639
|
+
"Get category %s form for node ID -> %s and category ID -> %s",
|
|
12640
|
+
operation,
|
|
12641
|
+
str(node_id),
|
|
12642
|
+
str(category_id),
|
|
12643
|
+
)
|
|
12644
|
+
|
|
12645
|
+
request_url = self.config()["nodesFormUrl"] + "/categories/{}?id={}&category_id={}".format(
|
|
12646
|
+
operation, node_id, category_id
|
|
12647
|
+
)
|
|
12648
|
+
|
|
12649
|
+
response = self.do_request(
|
|
12650
|
+
url=request_url,
|
|
12651
|
+
method="GET",
|
|
12652
|
+
headers=request_header,
|
|
12653
|
+
timeout=None,
|
|
12654
|
+
failure_message="Cannot get category {} form for node ID -> {} and category ID -> {}".format(
|
|
12655
|
+
operation,
|
|
12656
|
+
node_id,
|
|
12657
|
+
category_id,
|
|
12658
|
+
),
|
|
12659
|
+
)
|
|
12660
|
+
|
|
12661
|
+
return response
|
|
12662
|
+
|
|
12663
|
+
# end method definition
|
|
12664
|
+
|
|
12068
12665
|
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="set_system_attributes")
|
|
12069
12666
|
def set_system_attributes(
|
|
12070
12667
|
self,
|
|
@@ -12465,7 +13062,7 @@ class OTCS:
|
|
|
12465
13062
|
request_header = self.request_form_header()
|
|
12466
13063
|
|
|
12467
13064
|
self.logger.debug(
|
|
12468
|
-
"Running
|
|
13065
|
+
"Running web report with nickname -> '%s'; calling -> %s",
|
|
12469
13066
|
nickname,
|
|
12470
13067
|
request_url,
|
|
12471
13068
|
)
|
|
@@ -12503,7 +13100,7 @@ class OTCS:
|
|
|
12503
13100
|
request_header = self.request_form_header()
|
|
12504
13101
|
|
|
12505
13102
|
self.logger.debug(
|
|
12506
|
-
"Install
|
|
13103
|
+
"Install OTCS application -> '%s'; calling -> %s",
|
|
12507
13104
|
application_name,
|
|
12508
13105
|
request_url,
|
|
12509
13106
|
)
|
|
@@ -12514,7 +13111,7 @@ class OTCS:
|
|
|
12514
13111
|
headers=request_header,
|
|
12515
13112
|
data=install_cs_application_post_data,
|
|
12516
13113
|
timeout=None,
|
|
12517
|
-
failure_message="Failed to install
|
|
13114
|
+
failure_message="Failed to install OTCS application -> '{}'".format(
|
|
12518
13115
|
application_name,
|
|
12519
13116
|
),
|
|
12520
13117
|
)
|
|
@@ -12783,6 +13380,64 @@ class OTCS:
|
|
|
12783
13380
|
|
|
12784
13381
|
# end method definition
|
|
12785
13382
|
|
|
13383
|
+
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="check_user_node_permissions")
|
|
13384
|
+
def check_user_node_permissions(self, node_ids: list[int]) -> dict | None:
|
|
13385
|
+
"""Check if the current user has permissions to access a given list of Content Server nodes.
|
|
13386
|
+
|
|
13387
|
+
This is using the AI endpoint has this method is typically used in Aviator use cases.
|
|
13388
|
+
|
|
13389
|
+
Args:
|
|
13390
|
+
node_ids (list[int]):
|
|
13391
|
+
List of node IDs to check.
|
|
13392
|
+
|
|
13393
|
+
Returns:
|
|
13394
|
+
dict | None:
|
|
13395
|
+
REST API response or None in case of an error.
|
|
13396
|
+
|
|
13397
|
+
Example:
|
|
13398
|
+
{
|
|
13399
|
+
'links': {
|
|
13400
|
+
'data': {
|
|
13401
|
+
self': {
|
|
13402
|
+
'body': '',
|
|
13403
|
+
'content_type': '',
|
|
13404
|
+
'href': '/api/v2/ai/nodes/permissions/check',
|
|
13405
|
+
'method': 'POST',
|
|
13406
|
+
'name': ''
|
|
13407
|
+
}
|
|
13408
|
+
}
|
|
13409
|
+
},
|
|
13410
|
+
'results': {
|
|
13411
|
+
'ids': [...]
|
|
13412
|
+
}
|
|
13413
|
+
}
|
|
13414
|
+
|
|
13415
|
+
"""
|
|
13416
|
+
|
|
13417
|
+
request_url = self.config()["aiUrl"] + "/permissions/check"
|
|
13418
|
+
request_header = self.request_form_header()
|
|
13419
|
+
|
|
13420
|
+
permission_post_data = {"ids": node_ids}
|
|
13421
|
+
|
|
13422
|
+
if float(self.get_server_version()) < 25.4:
|
|
13423
|
+
permission_post_data["user_hash"] = self.otcs_ticket_hashed()
|
|
13424
|
+
|
|
13425
|
+
self.logger.debug(
|
|
13426
|
+
"Check if current user has permissions to access nodes -> %s; calling -> %s",
|
|
13427
|
+
node_ids,
|
|
13428
|
+
request_url,
|
|
13429
|
+
)
|
|
13430
|
+
|
|
13431
|
+
return self.do_request(
|
|
13432
|
+
url=request_url,
|
|
13433
|
+
method="POST",
|
|
13434
|
+
headers=request_header,
|
|
13435
|
+
data=permission_post_data,
|
|
13436
|
+
failure_message="Failed to check if current user has permissions to access nodes -> {}".format(node_ids),
|
|
13437
|
+
)
|
|
13438
|
+
|
|
13439
|
+
# end method definition
|
|
13440
|
+
|
|
12786
13441
|
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="get_node_categories")
|
|
12787
13442
|
def get_node_categories(self, node_id: int, metadata: bool = True) -> dict | None:
|
|
12788
13443
|
"""Get categories assigned to a node.
|
|
@@ -12946,7 +13601,7 @@ class OTCS:
|
|
|
12946
13601
|
|
|
12947
13602
|
Returns:
|
|
12948
13603
|
dict | None:
|
|
12949
|
-
REST
|
|
13604
|
+
REST response with category data or None if the call to the REST API fails.
|
|
12950
13605
|
|
|
12951
13606
|
"""
|
|
12952
13607
|
|
|
@@ -13207,9 +13862,129 @@ class OTCS:
|
|
|
13207
13862
|
set_name = None
|
|
13208
13863
|
set_id = None
|
|
13209
13864
|
|
|
13210
|
-
return cat_id, attribute_definitions
|
|
13865
|
+
return cat_id, attribute_definitions
|
|
13866
|
+
|
|
13867
|
+
return -1, {}
|
|
13868
|
+
|
|
13869
|
+
# end method definition
|
|
13870
|
+
|
|
13871
|
+
def get_category_id_by_name(self, node_id: int, category_name: str) -> int | None:
|
|
13872
|
+
"""Get the category ID by its name.
|
|
13873
|
+
|
|
13874
|
+
Args:
|
|
13875
|
+
node_id (int):
|
|
13876
|
+
The ID of the node to get the categories for.
|
|
13877
|
+
category_name (str):
|
|
13878
|
+
The name of the category to get the ID for.
|
|
13879
|
+
|
|
13880
|
+
Returns:
|
|
13881
|
+
int | None:
|
|
13882
|
+
The category ID or None if the category is not found.
|
|
13883
|
+
|
|
13884
|
+
"""
|
|
13885
|
+
|
|
13886
|
+
response = self.get_node_categories(node_id=node_id)
|
|
13887
|
+
results = response["results"]
|
|
13888
|
+
for result in results:
|
|
13889
|
+
categories = result["metadata"]["categories"]
|
|
13890
|
+
first_key = next(iter(categories))
|
|
13891
|
+
if categories[first_key]["name"] == category_name:
|
|
13892
|
+
return first_key
|
|
13893
|
+
return None
|
|
13894
|
+
|
|
13895
|
+
# end method definition
|
|
13896
|
+
|
|
13897
|
+
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="get_category_as_dictionary")
|
|
13898
|
+
def get_node_category_as_dictionary(
|
|
13899
|
+
self, node_id: int, category_id: int | None = None, category_name: str | None = None
|
|
13900
|
+
) -> dict | None:
|
|
13901
|
+
"""Get a specific category assigned to a node in a streamlined Python dictionary form.
|
|
13902
|
+
|
|
13903
|
+
* The whole category data of a node is embedded into a python dict.
|
|
13904
|
+
* Single-value / scalar attributes are key / value pairs in that dict.
|
|
13905
|
+
* Multi-value attributes become key / value pairs with value being a list of strings or integers.
|
|
13906
|
+
* Single-line sets become key /value pairs with value being a sub-dict.
|
|
13907
|
+
* Attribute in single-line sets become key / value pairs in the sub-dict.
|
|
13908
|
+
* Multi-line sets become key / value pairs with value being a list of dicts.
|
|
13909
|
+
* Single-value attributes in multi-line sets become key / value pairs inside the dict at the row position in the list.
|
|
13910
|
+
* Multi-value attributes in multi-line sets become key / value pairs inside the dict at the row position in the list
|
|
13911
|
+
with value being a list of strings or integers.
|
|
13912
|
+
|
|
13913
|
+
See also extract_category_data() for an alternative implementation.
|
|
13914
|
+
|
|
13915
|
+
Args:
|
|
13916
|
+
node_id (int):
|
|
13917
|
+
The ID of the node to get the categories for.
|
|
13918
|
+
category_id (int | None, optional):
|
|
13919
|
+
The node ID of the category definition (in category volume). If not provided,
|
|
13920
|
+
the category ID is determined by its name.
|
|
13921
|
+
category_name (str | None, optional):
|
|
13922
|
+
The name of the category to get the ID for.
|
|
13923
|
+
If category_id is not provided, the category ID is determined by its name.
|
|
13924
|
+
|
|
13925
|
+
Returns:
|
|
13926
|
+
dict | None:
|
|
13927
|
+
REST response with category data or None if the call to the REST API fails.
|
|
13928
|
+
|
|
13929
|
+
"""
|
|
13930
|
+
|
|
13931
|
+
if not category_id and not category_name:
|
|
13932
|
+
self.logger.error("Either category ID or category name must be provided!")
|
|
13933
|
+
return None
|
|
13934
|
+
|
|
13935
|
+
if not category_id:
|
|
13936
|
+
category_id = self.get_category_id_by_name(node_id=node_id, category_name=category_name)
|
|
13937
|
+
|
|
13938
|
+
response = self.get_node_category(node_id=node_id, category_id=category_id)
|
|
13939
|
+
|
|
13940
|
+
data = response["results"]["data"]["categories"]
|
|
13941
|
+
metadata = response["results"]["metadata"]["categories"]
|
|
13942
|
+
category_key = next(iter(metadata))
|
|
13943
|
+
_ = metadata.pop(category_key)
|
|
13944
|
+
|
|
13945
|
+
# Initialize the result dict:
|
|
13946
|
+
result = {}
|
|
13211
13947
|
|
|
13212
|
-
|
|
13948
|
+
for key, attribute in metadata.items():
|
|
13949
|
+
is_set = attribute["persona"] == "set"
|
|
13950
|
+
is_multi_value = attribute["multi_value"]
|
|
13951
|
+
attr_name = attribute["name"]
|
|
13952
|
+
attr_key = attribute["key"]
|
|
13953
|
+
|
|
13954
|
+
if is_set:
|
|
13955
|
+
set_name = attr_name
|
|
13956
|
+
set_multi_value = is_multi_value
|
|
13957
|
+
|
|
13958
|
+
if not is_set and "x" not in attr_key:
|
|
13959
|
+
result[attr_name] = data[key]
|
|
13960
|
+
set_name = None
|
|
13961
|
+
elif is_set:
|
|
13962
|
+
# The current attribute is the set itself:
|
|
13963
|
+
if not is_multi_value:
|
|
13964
|
+
result[attr_name] = {}
|
|
13965
|
+
else:
|
|
13966
|
+
result[attr_name] = []
|
|
13967
|
+
set_name = attr_name
|
|
13968
|
+
elif not is_set and "x" in attr_key:
|
|
13969
|
+
# We re inside a set and process the set attributes:
|
|
13970
|
+
if not set_multi_value:
|
|
13971
|
+
# A single row set:
|
|
13972
|
+
attr_key = attr_key.replace("_x_", "_1_")
|
|
13973
|
+
result[set_name][attr_name] = data[attr_key]
|
|
13974
|
+
else:
|
|
13975
|
+
# Collect all the row data:
|
|
13976
|
+
for index in range(1, 50):
|
|
13977
|
+
attr_key_index = attr_key.replace("_x_", "_" + str(index) + "_")
|
|
13978
|
+
# Do we have data for this row?
|
|
13979
|
+
if attr_key_index in data:
|
|
13980
|
+
if index > len(result[set_name]):
|
|
13981
|
+
result[set_name].append({attr_name: data[attr_key_index]})
|
|
13982
|
+
else:
|
|
13983
|
+
result[set_name][index - 1][attr_name] = data[attr_key_index]
|
|
13984
|
+
else:
|
|
13985
|
+
# No more rows
|
|
13986
|
+
break
|
|
13987
|
+
return result
|
|
13213
13988
|
|
|
13214
13989
|
# end method definition
|
|
13215
13990
|
|
|
@@ -13735,6 +14510,8 @@ class OTCS:
|
|
|
13735
14510
|
* Multi-value attributes in multi-line sets become key / value pairs inside the dict at the row position in the list
|
|
13736
14511
|
with value being a list of strings or integers.
|
|
13737
14512
|
|
|
14513
|
+
See also get_node_category_as_dictionary() for an alternative implementation.
|
|
14514
|
+
|
|
13738
14515
|
Args:
|
|
13739
14516
|
node (dict):
|
|
13740
14517
|
The typical node response of a node get REST API call that include the "categories" fields.
|
|
@@ -13806,14 +14583,37 @@ class OTCS:
|
|
|
13806
14583
|
|
|
13807
14584
|
# Start of main method body:
|
|
13808
14585
|
|
|
13809
|
-
if not node
|
|
14586
|
+
if not node:
|
|
14587
|
+
self.logger.error("Cannot extract category data. No node data provided!")
|
|
13810
14588
|
return None
|
|
13811
14589
|
|
|
14590
|
+
if "results" not in node:
|
|
14591
|
+
# Support also iterators that have resolved the "results" already.
|
|
14592
|
+
# In this case we wrap it in a "rsults" dict to make it look like
|
|
14593
|
+
# a full response:
|
|
14594
|
+
if "data" in node:
|
|
14595
|
+
node = {"results": node}
|
|
14596
|
+
else:
|
|
14597
|
+
return None
|
|
14598
|
+
|
|
14599
|
+
# Some OTCS REST APIs may return a list of nodes in "results".
|
|
14600
|
+
# We only support processing a single node here:
|
|
14601
|
+
if isinstance(node["results"], list):
|
|
14602
|
+
if len(node["results"]) > 1:
|
|
14603
|
+
self.logger.warning("Response includes a node list. Extracting category data for the first node!")
|
|
14604
|
+
node["results"] = node["results"][0]
|
|
14605
|
+
|
|
13812
14606
|
if "metadata" not in node["results"]:
|
|
13813
|
-
self.logger.error("Cannot
|
|
14607
|
+
self.logger.error("Cannot extract category data. Method was called without the '&metadata' parameter!")
|
|
13814
14608
|
return None
|
|
13815
14609
|
|
|
13816
|
-
|
|
14610
|
+
metadata = node["results"]["metadata"]
|
|
14611
|
+
if "categories" not in metadata:
|
|
14612
|
+
self.logger.error(
|
|
14613
|
+
"Cannot extract category data. No category data found in node response! Use 'categories' value for 'fields' parameter in the node call!"
|
|
14614
|
+
)
|
|
14615
|
+
return None
|
|
14616
|
+
category_schemas = metadata["categories"]
|
|
13817
14617
|
|
|
13818
14618
|
result_dict = {}
|
|
13819
14619
|
current_dict = result_dict
|
|
@@ -13821,6 +14621,15 @@ class OTCS:
|
|
|
13821
14621
|
category_lookup = {}
|
|
13822
14622
|
attribute_lookup = {}
|
|
13823
14623
|
|
|
14624
|
+
# Some REST API return categories in different format. We adjust
|
|
14625
|
+
# it on the fly here:
|
|
14626
|
+
if isinstance(category_schemas, list):
|
|
14627
|
+
new_schema = {}
|
|
14628
|
+
for category_schema in category_schemas:
|
|
14629
|
+
first_key = next(iter(category_schema))
|
|
14630
|
+
new_schema[first_key] = category_schema
|
|
14631
|
+
category_schemas = new_schema
|
|
14632
|
+
|
|
13824
14633
|
try:
|
|
13825
14634
|
for category_key, category_schema in category_schemas.items():
|
|
13826
14635
|
for attribute_key, attribute_schema in category_schema.items():
|
|
@@ -13888,9 +14697,17 @@ class OTCS:
|
|
|
13888
14697
|
self.logger.error("Type -> '%s' not handled yet!", attribute_type)
|
|
13889
14698
|
except Exception as e:
|
|
13890
14699
|
self.logger.error("Something went wrong with getting the data schema! Error -> %s", str(e))
|
|
14700
|
+
return None
|
|
13891
14701
|
|
|
13892
14702
|
category_datas = node["results"]["data"]["categories"]
|
|
13893
14703
|
|
|
14704
|
+
if isinstance(category_datas, list):
|
|
14705
|
+
new_data = {}
|
|
14706
|
+
for category_data in category_datas:
|
|
14707
|
+
first_key = next(iter(category_data))
|
|
14708
|
+
new_data[first_key] = category_data
|
|
14709
|
+
category_datas = new_data
|
|
14710
|
+
|
|
13894
14711
|
try:
|
|
13895
14712
|
for category_data in category_datas.values():
|
|
13896
14713
|
for attribute_key, value in category_data.items():
|
|
@@ -13911,7 +14728,7 @@ class OTCS:
|
|
|
13911
14728
|
current_dict = set_data
|
|
13912
14729
|
elif isinstance(set_data, list):
|
|
13913
14730
|
row_number = get_row_number(attribute_key=attribute_key)
|
|
13914
|
-
self.logger.debug("
|
|
14731
|
+
self.logger.debug("Target dict is row %d of multi-line set attribute.", row_number)
|
|
13915
14732
|
if row_number > len(set_data):
|
|
13916
14733
|
self.logger.debug("Add rows up to %d of multi-line set attribute...", row_number)
|
|
13917
14734
|
for _ in range(row_number - len(set_data)):
|
|
@@ -13927,6 +14744,7 @@ class OTCS:
|
|
|
13927
14744
|
current_dict[attribute_name] = value
|
|
13928
14745
|
except Exception as e:
|
|
13929
14746
|
self.logger.error("Something went wrong while filling the data! Error -> %s", str(e))
|
|
14747
|
+
return None
|
|
13930
14748
|
|
|
13931
14749
|
return result_dict
|
|
13932
14750
|
|
|
@@ -14311,7 +15129,7 @@ class OTCS:
|
|
|
14311
15129
|
|
|
14312
15130
|
Args:
|
|
14313
15131
|
node_id (int):
|
|
14314
|
-
The node ID of the
|
|
15132
|
+
The node ID of the Business Workspace template.
|
|
14315
15133
|
|
|
14316
15134
|
Returns:
|
|
14317
15135
|
dict | None:
|
|
@@ -16679,7 +17497,7 @@ class OTCS:
|
|
|
16679
17497
|
"enabled": status,
|
|
16680
17498
|
}
|
|
16681
17499
|
|
|
16682
|
-
request_url = self.config()["
|
|
17500
|
+
request_url = self.config()["aiNodesUrl"] + "/{}".format(workspace_id)
|
|
16683
17501
|
request_header = self.request_form_header()
|
|
16684
17502
|
|
|
16685
17503
|
if status is True:
|
|
@@ -16708,6 +17526,181 @@ class OTCS:
|
|
|
16708
17526
|
|
|
16709
17527
|
# end method definition
|
|
16710
17528
|
|
|
17529
|
+
def aviator_chat(
|
|
17530
|
+
self, context: str | None, messages: list[dict], where: list[dict] | None = None, inline_citation: bool = True
|
|
17531
|
+
) -> dict | None:
|
|
17532
|
+
"""Process a chat interaction with Content Aviator.
|
|
17533
|
+
|
|
17534
|
+
Args:
|
|
17535
|
+
context (str | None):
|
|
17536
|
+
Context for the current conversation. This includes the text chunks
|
|
17537
|
+
provided by the RAG pipeline.
|
|
17538
|
+
messages (list[dict]):
|
|
17539
|
+
List of messages from conversation history.
|
|
17540
|
+
Format example:
|
|
17541
|
+
[
|
|
17542
|
+
{
|
|
17543
|
+
"author": "user", "content": "Summarize this workspace, please."
|
|
17544
|
+
},
|
|
17545
|
+
{
|
|
17546
|
+
"author": "ai", "content": "..."
|
|
17547
|
+
}
|
|
17548
|
+
]
|
|
17549
|
+
where (list):
|
|
17550
|
+
Metadata name/value pairs for the query.
|
|
17551
|
+
Could be used to specify workspaces, documents, or other criteria in the future.
|
|
17552
|
+
Values need to match those passed as metadata to the embeddings API.
|
|
17553
|
+
Format example:
|
|
17554
|
+
[
|
|
17555
|
+
{"workspaceID":"38673"},
|
|
17556
|
+
{"documentID":"38458"},
|
|
17557
|
+
]
|
|
17558
|
+
inline_citation (bool, optional):
|
|
17559
|
+
Whether or not inline citations should be used in the response. Default is True.
|
|
17560
|
+
|
|
17561
|
+
Returns:
|
|
17562
|
+
dict:
|
|
17563
|
+
Conversation status
|
|
17564
|
+
|
|
17565
|
+
Example:
|
|
17566
|
+
{
|
|
17567
|
+
'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.',
|
|
17568
|
+
'references': [
|
|
17569
|
+
{
|
|
17570
|
+
'chunks': [
|
|
17571
|
+
{
|
|
17572
|
+
'citation': None,
|
|
17573
|
+
'content': ['16. 1 Basic principles 16.'],
|
|
17574
|
+
'distance': 0.262610273676197,
|
|
17575
|
+
'source': 'Similarity'
|
|
17576
|
+
}
|
|
17577
|
+
],
|
|
17578
|
+
'distance': 0.262610273676197,
|
|
17579
|
+
'metadata': {
|
|
17580
|
+
'content': {
|
|
17581
|
+
'chunks': ['16. 1 Basic principles 16.'],
|
|
17582
|
+
'source': 'Similarity'
|
|
17583
|
+
},
|
|
17584
|
+
'documentID': '39004',
|
|
17585
|
+
'workspaceID': '38673'
|
|
17586
|
+
}
|
|
17587
|
+
},
|
|
17588
|
+
{
|
|
17589
|
+
'chunks': [
|
|
17590
|
+
{
|
|
17591
|
+
'citation': None,
|
|
17592
|
+
'content': ['16. 1.'],
|
|
17593
|
+
'distance': 0.284182507756566,
|
|
17594
|
+
'source': 'Similarity'
|
|
17595
|
+
}
|
|
17596
|
+
],
|
|
17597
|
+
'distance': 0.284182507756566,
|
|
17598
|
+
'metadata': {
|
|
17599
|
+
'content': {
|
|
17600
|
+
'chunks': ['16. 1.'],
|
|
17601
|
+
'source': 'Similarity'
|
|
17602
|
+
},
|
|
17603
|
+
'documentID': '38123',
|
|
17604
|
+
'workspaceID': '38673'
|
|
17605
|
+
}
|
|
17606
|
+
}
|
|
17607
|
+
],
|
|
17608
|
+
'context': 'Tool "get_context" called with arguments {"query":"Tell me about the calibration equipment"} and returned:',
|
|
17609
|
+
'queryMetadata': {
|
|
17610
|
+
'originalQuery': 'Tell me about the calibration equipment',
|
|
17611
|
+
'usedQuery': 'Tell me about the calibration equipment'
|
|
17612
|
+
}
|
|
17613
|
+
}
|
|
17614
|
+
|
|
17615
|
+
|
|
17616
|
+
|
|
17617
|
+
"""
|
|
17618
|
+
|
|
17619
|
+
request_url = self.config()["aiChatUrl"]
|
|
17620
|
+
request_header = self.request_form_header()
|
|
17621
|
+
|
|
17622
|
+
chat_data = {}
|
|
17623
|
+
if where:
|
|
17624
|
+
chat_data["where"] = where
|
|
17625
|
+
|
|
17626
|
+
chat_data["context"] = context
|
|
17627
|
+
chat_data["messages"] = messages
|
|
17628
|
+
# "synonyms": self.config()["synonyms"],
|
|
17629
|
+
chat_data["inlineCitation"] = inline_citation
|
|
17630
|
+
|
|
17631
|
+
return self.do_request(
|
|
17632
|
+
url=request_url,
|
|
17633
|
+
method="POST",
|
|
17634
|
+
headers=request_header,
|
|
17635
|
+
# data=chat_data,
|
|
17636
|
+
data={"body": json.dumps(chat_data)},
|
|
17637
|
+
timeout=None,
|
|
17638
|
+
failure_message="Failed to chat with Content Aviator",
|
|
17639
|
+
)
|
|
17640
|
+
|
|
17641
|
+
# end method definition
|
|
17642
|
+
|
|
17643
|
+
def aviator_context(
|
|
17644
|
+
self, query: str, threshold: float = 0.5, limit: int = 10, data: list | None = None
|
|
17645
|
+
) -> dict | None:
|
|
17646
|
+
"""Get context based on the query text from Aviator's vector database.
|
|
17647
|
+
|
|
17648
|
+
Results are text-chunks and they will be permission-checked for the authenticated user.
|
|
17649
|
+
|
|
17650
|
+
Args:
|
|
17651
|
+
query (str):
|
|
17652
|
+
The query text to search for similar text chunks.
|
|
17653
|
+
threshold (float, optional):
|
|
17654
|
+
Similarity threshold between 0 and 1. Default is 0.5.
|
|
17655
|
+
limit (int, optional):
|
|
17656
|
+
Maximum number of results to return. Default is 10.
|
|
17657
|
+
data (list | None, optional):
|
|
17658
|
+
Additional data to pass to the embeddings API. Defaults to None.
|
|
17659
|
+
This can include metadata for filtering the results.
|
|
17660
|
+
|
|
17661
|
+
Returns:
|
|
17662
|
+
dict | None:
|
|
17663
|
+
The response from the embeddings API or None if the request fails.
|
|
17664
|
+
|
|
17665
|
+
"""
|
|
17666
|
+
|
|
17667
|
+
request_url = self.config()["aiContextUrl"]
|
|
17668
|
+
request_header = self.request_form_header()
|
|
17669
|
+
|
|
17670
|
+
if not query:
|
|
17671
|
+
self.logger.error("Query text is required for getting context from Content Aviator!")
|
|
17672
|
+
return None
|
|
17673
|
+
|
|
17674
|
+
context_post_body = {
|
|
17675
|
+
"query": query,
|
|
17676
|
+
"threshold": threshold,
|
|
17677
|
+
"limit": limit,
|
|
17678
|
+
}
|
|
17679
|
+
if data:
|
|
17680
|
+
context_post_body["data"] = data
|
|
17681
|
+
else:
|
|
17682
|
+
context_post_body["data"] = []
|
|
17683
|
+
|
|
17684
|
+
self.logger.debug(
|
|
17685
|
+
"Get context from Content Aviator for query -> '%s' (threshold: %f, limit: %d); calling -> %s",
|
|
17686
|
+
query,
|
|
17687
|
+
threshold,
|
|
17688
|
+
limit,
|
|
17689
|
+
request_url,
|
|
17690
|
+
)
|
|
17691
|
+
|
|
17692
|
+
return self.do_request(
|
|
17693
|
+
url=request_url,
|
|
17694
|
+
method="POST",
|
|
17695
|
+
headers=request_header,
|
|
17696
|
+
# data={"body": json.dumps(context_post_body)},
|
|
17697
|
+
data=context_post_body,
|
|
17698
|
+
timeout=None,
|
|
17699
|
+
failure_message="Failed to retrieve context from Content Aviator",
|
|
17700
|
+
)
|
|
17701
|
+
|
|
17702
|
+
# end method definition
|
|
17703
|
+
|
|
16711
17704
|
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="traverse_node")
|
|
16712
17705
|
def traverse_node(
|
|
16713
17706
|
self,
|
|
@@ -16788,7 +17781,8 @@ class OTCS:
|
|
|
16788
17781
|
for subnode in subnodes:
|
|
16789
17782
|
subnode_id = self.get_result_value(response=subnode, key="id")
|
|
16790
17783
|
subnode_name = self.get_result_value(response=subnode, key="name")
|
|
16791
|
-
self.
|
|
17784
|
+
subnode_type = self.get_result_value(response=subnode, key="type")
|
|
17785
|
+
self.logger.info("Traversing %s node -> '%s' (%s)", subnode_type, subnode_name, subnode_id)
|
|
16792
17786
|
# Recursive call for current subnode:
|
|
16793
17787
|
result = self.traverse_node(
|
|
16794
17788
|
node=subnode,
|
|
@@ -16866,7 +17860,13 @@ class OTCS:
|
|
|
16866
17860
|
task_queue.put((subnode, 0, traversal_data))
|
|
16867
17861
|
|
|
16868
17862
|
def traverse_node_worker() -> None:
|
|
16869
|
-
"""Work on queue.
|
|
17863
|
+
"""Work on a shared queue.
|
|
17864
|
+
|
|
17865
|
+
Loops over these steps:
|
|
17866
|
+
1. Get node from queue
|
|
17867
|
+
2. Execute all executables for that node
|
|
17868
|
+
3. If node is a container and executables indicate to traverse,
|
|
17869
|
+
then enqueue all subnodes
|
|
16870
17870
|
|
|
16871
17871
|
Returns:
|
|
16872
17872
|
None
|
|
@@ -16903,6 +17903,17 @@ class OTCS:
|
|
|
16903
17903
|
|
|
16904
17904
|
# Run all executables
|
|
16905
17905
|
for executable in executables or []:
|
|
17906
|
+
# The executables are functions or method from outside this class.
|
|
17907
|
+
# They need to return a tuple of two boolean values:
|
|
17908
|
+
# (result_success, result_traverse)
|
|
17909
|
+
# result_success indicates if the executable was successful (True)
|
|
17910
|
+
# or not (False). If False, the execution of the executables list
|
|
17911
|
+
# is stopped.
|
|
17912
|
+
# result_traverse indicates if the traversal should continue
|
|
17913
|
+
# into subnodes (True) or not (False).
|
|
17914
|
+
# If at least one executable returns result_traverse = True,
|
|
17915
|
+
# then the traversal into subnodes will be done (if the node is a container).
|
|
17916
|
+
# As this code is from outside this class, we better catch exceptions:
|
|
16906
17917
|
try:
|
|
16907
17918
|
result_success, result_traverse = executable(
|
|
16908
17919
|
node=node,
|
|
@@ -17877,7 +18888,8 @@ class OTCS:
|
|
|
17877
18888
|
Defaults to None = filter not active.
|
|
17878
18889
|
filter_subtypes (list | None, optional):
|
|
17879
18890
|
Additive filter criterium for item type.
|
|
17880
|
-
Defaults to None = filter not active.
|
|
18891
|
+
Defaults to None = filter not active. filter_subtypes = [] is different from None!
|
|
18892
|
+
If an empty list is provided, the filter is effectively always True.
|
|
17881
18893
|
filter_category (str | None, optional):
|
|
17882
18894
|
Additive filter criterium for existence of a category on the node.
|
|
17883
18895
|
The value of filter_category is the name of the category
|
|
@@ -17910,7 +18922,7 @@ class OTCS:
|
|
|
17910
18922
|
self.logger.error("Illegal node - cannot apply filter!")
|
|
17911
18923
|
return False
|
|
17912
18924
|
|
|
17913
|
-
if filter_subtypes and node["type"] not in filter_subtypes:
|
|
18925
|
+
if filter_subtypes is not None and node["type"] not in filter_subtypes:
|
|
17914
18926
|
self.logger.debug(
|
|
17915
18927
|
"Node type -> '%s' is not in filter node types -> %s. Node -> '%s' failed filter test.",
|
|
17916
18928
|
node["type"],
|
|
@@ -18623,6 +19635,8 @@ class OTCS:
|
|
|
18623
19635
|
def load_items(
|
|
18624
19636
|
self,
|
|
18625
19637
|
node_id: int,
|
|
19638
|
+
workspaces: bool = True,
|
|
19639
|
+
items: bool = True,
|
|
18626
19640
|
filter_workspace_depth: int | None = None,
|
|
18627
19641
|
filter_workspace_subtypes: list | None = None,
|
|
18628
19642
|
filter_workspace_category: str | None = None,
|
|
@@ -18647,6 +19661,12 @@ class OTCS:
|
|
|
18647
19661
|
Args:
|
|
18648
19662
|
node_id (int):
|
|
18649
19663
|
The root Node ID the traversal should start at.
|
|
19664
|
+
workspaces (bool, optional):
|
|
19665
|
+
If True, workspaces are included in the data frame.
|
|
19666
|
+
Defaults to True.
|
|
19667
|
+
items (bool, optional):
|
|
19668
|
+
If True, document items are included in the data frame.
|
|
19669
|
+
Defaults to True.
|
|
18650
19670
|
filter_workspace_depth (int | None, optional):
|
|
18651
19671
|
Additive filter criterium for workspace path depth.
|
|
18652
19672
|
Defaults to None = filter not active.
|
|
@@ -18695,9 +19715,30 @@ class OTCS:
|
|
|
18695
19715
|
dict:
|
|
18696
19716
|
Stats with processed and traversed counters.
|
|
18697
19717
|
|
|
19718
|
+
Side Effects:
|
|
19719
|
+
The resulting data frame is stored in self._data. It will have the following columns:
|
|
19720
|
+
- type which is either "item" or "workspace"
|
|
19721
|
+
- workspace_type
|
|
19722
|
+
- workspace_id
|
|
19723
|
+
- workspace_name
|
|
19724
|
+
- workspace_description
|
|
19725
|
+
- workspace_outer_path
|
|
19726
|
+
- workspace_<cat_id>_<attr_id> for each workspace attribute if workspace_metadata is True
|
|
19727
|
+
- item_id
|
|
19728
|
+
- item_type
|
|
19729
|
+
- item_name
|
|
19730
|
+
- item_description
|
|
19731
|
+
- item_path
|
|
19732
|
+
- item_download_name
|
|
19733
|
+
- item_mime_type
|
|
19734
|
+
- item_url
|
|
19735
|
+
- item_<cat_id>_<attr_id> for each item attribute if item_metadata is True
|
|
19736
|
+
- item_cat_<cat_id>_<attr_id> for each item attribute if item_metadata is True and self._use_numeric_category_identifier is True
|
|
19737
|
+
- item_cat_<cat_name>_<attr_name> for each item attribute if item_metadata is True and self._use_numeric_category_identifier is False
|
|
19738
|
+
|
|
18698
19739
|
"""
|
|
18699
19740
|
|
|
18700
|
-
# Initiaze download threads for
|
|
19741
|
+
# Initiaze download threads for document items:
|
|
18701
19742
|
download_threads = []
|
|
18702
19743
|
|
|
18703
19744
|
def check_node_exclusions(node: dict, **kwargs: dict) -> tuple[bool, bool]:
|
|
@@ -18718,15 +19759,14 @@ class OTCS:
|
|
|
18718
19759
|
|
|
18719
19760
|
"""
|
|
18720
19761
|
|
|
18721
|
-
|
|
18722
|
-
|
|
18723
|
-
|
|
18724
|
-
return (False, False)
|
|
19762
|
+
# Get the list of node IDs to exclude from the keyword arguments.
|
|
19763
|
+
# If not provided, use an empty list as default which means no exclusions.
|
|
19764
|
+
exclude_node_ids = kwargs.get("exclude_node_ids") or []
|
|
18725
19765
|
|
|
18726
19766
|
node_id = self.get_result_value(response=node, key="id")
|
|
18727
19767
|
node_name = self.get_result_value(response=node, key="name")
|
|
18728
19768
|
|
|
18729
|
-
if node_id and
|
|
19769
|
+
if node_id and (node_id in exclude_node_ids):
|
|
18730
19770
|
self.logger.info(
|
|
18731
19771
|
"Node -> '%s' (%s) is in exclusion list. Skip traversal of this node.",
|
|
18732
19772
|
node_name,
|
|
@@ -18753,13 +19793,36 @@ class OTCS:
|
|
|
18753
19793
|
|
|
18754
19794
|
"""
|
|
18755
19795
|
|
|
19796
|
+
# This should actually not happen as the caller should
|
|
19797
|
+
# check if workspaces are requested before calling this function.
|
|
19798
|
+
if not workspaces:
|
|
19799
|
+
# Success = False, Traverse = True
|
|
19800
|
+
return (False, True)
|
|
19801
|
+
|
|
18756
19802
|
traversal_data = kwargs.get("traversal_data")
|
|
18757
19803
|
filter_workspace_data = kwargs.get("filter_workspace_data")
|
|
18758
19804
|
control_flags = kwargs.get("control_flags")
|
|
18759
19805
|
|
|
18760
|
-
if not traversal_data
|
|
18761
|
-
self.logger.error(
|
|
18762
|
-
|
|
19806
|
+
if not traversal_data:
|
|
19807
|
+
self.logger.error(
|
|
19808
|
+
"Missing keyword argument 'traversal_data' for executable 'check_node_workspace' in node traversal!"
|
|
19809
|
+
)
|
|
19810
|
+
# Success = False, Traverse = False
|
|
19811
|
+
return (False, False)
|
|
19812
|
+
|
|
19813
|
+
if not filter_workspace_data:
|
|
19814
|
+
self.logger.error(
|
|
19815
|
+
"Missing keyword argument 'filter_workspace_data' for executable 'check_node_workspace' in node traversal!"
|
|
19816
|
+
)
|
|
19817
|
+
# Success = False, Traverse = False
|
|
19818
|
+
return (False, False)
|
|
19819
|
+
|
|
19820
|
+
if not control_flags:
|
|
19821
|
+
self.logger.error(
|
|
19822
|
+
"Missing keyword argument 'control_flags' for executable 'check_node_workspace' in node traversal!"
|
|
19823
|
+
)
|
|
19824
|
+
# Success = False, Traverse = False
|
|
19825
|
+
return (False, False)
|
|
18763
19826
|
|
|
18764
19827
|
node_id = self.get_result_value(response=node, key="id")
|
|
18765
19828
|
node_name = self.get_result_value(response=node, key="name")
|
|
@@ -18767,10 +19830,10 @@ class OTCS:
|
|
|
18767
19830
|
node_type = self.get_result_value(response=node, key="type")
|
|
18768
19831
|
|
|
18769
19832
|
#
|
|
18770
|
-
# 1. Check if the traversal is already inside a
|
|
18771
|
-
# the workspace processing
|
|
19833
|
+
# 1. Check if the traversal is already inside a workspace. Then we can skip
|
|
19834
|
+
# the workspace processing as we currently don't support sub-workspaces.
|
|
18772
19835
|
#
|
|
18773
|
-
workspace_id = traversal_data
|
|
19836
|
+
workspace_id = traversal_data.get("workspace_id")
|
|
18774
19837
|
if workspace_id:
|
|
18775
19838
|
self.logger.debug(
|
|
18776
19839
|
"Found folder or workspace -> '%s' (%s) inside workspace with ID -> %d. So this container cannot be a workspace.",
|
|
@@ -18801,7 +19864,7 @@ class OTCS:
|
|
|
18801
19864
|
categories = None
|
|
18802
19865
|
|
|
18803
19866
|
#
|
|
18804
|
-
# 3. Apply the defined filters to the current node to see
|
|
19867
|
+
# 3. Apply the defined workspace filters to the current node to see
|
|
18805
19868
|
# if we want to 'interpret' it as a workspace
|
|
18806
19869
|
#
|
|
18807
19870
|
# See if it is a node that we want to interpret as a workspace.
|
|
@@ -18818,6 +19881,13 @@ class OTCS:
|
|
|
18818
19881
|
filter_category=filter_workspace_data["filter_workspace_category"],
|
|
18819
19882
|
filter_attributes=filter_workspace_data["filter_workspace_attributes"],
|
|
18820
19883
|
):
|
|
19884
|
+
self.logger.debug(
|
|
19885
|
+
"Node -> '%s' (%s) did not match workspace filter -> %s",
|
|
19886
|
+
node_name,
|
|
19887
|
+
node_id,
|
|
19888
|
+
str(filter_workspace_data),
|
|
19889
|
+
)
|
|
19890
|
+
|
|
18821
19891
|
# Success = False, Traverse = True
|
|
18822
19892
|
return (False, True)
|
|
18823
19893
|
|
|
@@ -18831,7 +19901,7 @@ class OTCS:
|
|
|
18831
19901
|
#
|
|
18832
19902
|
# 4. Create the data frame row from the node / traversal data:
|
|
18833
19903
|
#
|
|
18834
|
-
row = {}
|
|
19904
|
+
row = {"type": "workspace"}
|
|
18835
19905
|
row["workspace_type"] = node_type
|
|
18836
19906
|
row["workspace_id"] = node_id
|
|
18837
19907
|
row["workspace_name"] = node_name
|
|
@@ -18855,7 +19925,7 @@ class OTCS:
|
|
|
18855
19925
|
traversal_data["workspace_description"] = node_description
|
|
18856
19926
|
self.logger.debug("Updated traversal data -> %s", str(traversal_data))
|
|
18857
19927
|
|
|
18858
|
-
# Success = True, Traverse =
|
|
19928
|
+
# Success = True, Traverse = True
|
|
18859
19929
|
# We have traverse = True because we need to
|
|
18860
19930
|
# keep traversing into the workspace folders.
|
|
18861
19931
|
return (True, True)
|
|
@@ -18882,8 +19952,16 @@ class OTCS:
|
|
|
18882
19952
|
filter_item_data = kwargs.get("filter_item_data")
|
|
18883
19953
|
control_flags = kwargs.get("control_flags")
|
|
18884
19954
|
|
|
18885
|
-
if not traversal_data
|
|
18886
|
-
self.logger.error("Missing keyword
|
|
19955
|
+
if not traversal_data:
|
|
19956
|
+
self.logger.error("Missing keyword argument 'traversal_data' for executable in node item traversal!")
|
|
19957
|
+
return (False, False)
|
|
19958
|
+
|
|
19959
|
+
if not filter_item_data:
|
|
19960
|
+
self.logger.error("Missing keyword argument 'filter_item_data' for executable in node item traversal!")
|
|
19961
|
+
return (False, False)
|
|
19962
|
+
|
|
19963
|
+
if not control_flags:
|
|
19964
|
+
self.logger.error("Missing keyword argument 'control_flags' for executable in node item traversal!")
|
|
18887
19965
|
return (False, False)
|
|
18888
19966
|
|
|
18889
19967
|
node_id = self.get_result_value(response=node, key="id")
|
|
@@ -18918,7 +19996,7 @@ class OTCS:
|
|
|
18918
19996
|
categories = None
|
|
18919
19997
|
|
|
18920
19998
|
#
|
|
18921
|
-
# 2. Apply the defined filters to the current node to see
|
|
19999
|
+
# 2. Apply the defined item filters to the current node to see
|
|
18922
20000
|
# if we want to add it to the data frame as an item.
|
|
18923
20001
|
#
|
|
18924
20002
|
# If filter_item_in_workspace is false, then documents
|
|
@@ -18935,10 +20013,17 @@ class OTCS:
|
|
|
18935
20013
|
filter_category=filter_item_data["filter_item_category"],
|
|
18936
20014
|
filter_attributes=filter_item_data["filter_item_attributes"],
|
|
18937
20015
|
):
|
|
20016
|
+
self.logger.debug(
|
|
20017
|
+
"Node -> '%s' (%s) did not match item filter -> %s",
|
|
20018
|
+
node_name,
|
|
20019
|
+
node_id,
|
|
20020
|
+
str(filter_item_data),
|
|
20021
|
+
)
|
|
20022
|
+
|
|
18938
20023
|
# Success = False, Traverse = True
|
|
18939
20024
|
return (False, True)
|
|
18940
20025
|
|
|
18941
|
-
#
|
|
20026
|
+
# Debug output where we found the item (inside or outside of workspace):
|
|
18942
20027
|
if workspace_id:
|
|
18943
20028
|
self.logger.debug(
|
|
18944
20029
|
"Found %s item -> '%s' (%s) in depth -> %s inside workspace -> '%s' (%s).",
|
|
@@ -18971,15 +20056,19 @@ class OTCS:
|
|
|
18971
20056
|
if control_flags["download_documents"] and (
|
|
18972
20057
|
not os.path.exists(file_path) or not control_flags["skip_existing_downloads"]
|
|
18973
20058
|
):
|
|
18974
|
-
|
|
18975
|
-
|
|
18976
|
-
#
|
|
20059
|
+
mime_type = self.get_result_value(response=node, key="mime_type")
|
|
20060
|
+
extract_after_download = mime_type == "application/x-zip-compressed" and extract_zip
|
|
18977
20061
|
self.logger.debug(
|
|
18978
|
-
"Downloading file -> '%s'...",
|
|
20062
|
+
"Downloading document -> '%s' (%s) to temp file -> '%s'%s...",
|
|
20063
|
+
node_name,
|
|
20064
|
+
mime_type,
|
|
18979
20065
|
file_path,
|
|
20066
|
+
" and extracting it after download" if extract_after_download else "",
|
|
18980
20067
|
)
|
|
18981
20068
|
|
|
18982
|
-
|
|
20069
|
+
#
|
|
20070
|
+
# Start asynchronous Download Thread:
|
|
20071
|
+
#
|
|
18983
20072
|
thread = threading.Thread(
|
|
18984
20073
|
target=self.download_document_multi_threading,
|
|
18985
20074
|
args=(node_id, file_path, extract_after_download),
|
|
@@ -18989,7 +20078,8 @@ class OTCS:
|
|
|
18989
20078
|
download_threads.append(thread)
|
|
18990
20079
|
else:
|
|
18991
20080
|
self.logger.debug(
|
|
18992
|
-
"
|
|
20081
|
+
"Document -> '%s' has been downloaded to file -> %s before or download is not requested. Skipping download...",
|
|
20082
|
+
node_name,
|
|
18993
20083
|
file_path,
|
|
18994
20084
|
)
|
|
18995
20085
|
# end if document
|
|
@@ -18998,26 +20088,36 @@ class OTCS:
|
|
|
18998
20088
|
# Construct a dictionary 'row' that we will add
|
|
18999
20089
|
# to the resulting data frame:
|
|
19000
20090
|
#
|
|
19001
|
-
row = {}
|
|
19002
|
-
|
|
19003
|
-
|
|
19004
|
-
|
|
19005
|
-
|
|
19006
|
-
|
|
19007
|
-
|
|
20091
|
+
row = {"type": "item"}
|
|
20092
|
+
if workspaces:
|
|
20093
|
+
# First we include some key workspace data to associate
|
|
20094
|
+
# the item with the workspace:
|
|
20095
|
+
row["workspace_type"] = workspace_type
|
|
20096
|
+
row["workspace_id"] = workspace_id
|
|
20097
|
+
row["workspace_name"] = workspace_name
|
|
20098
|
+
row["workspace_description"] = workspace_description
|
|
19008
20099
|
# Then add item specific data:
|
|
19009
20100
|
row["item_id"] = str(node_id)
|
|
19010
20101
|
row["item_type"] = node_type
|
|
19011
20102
|
row["item_name"] = node_name
|
|
19012
20103
|
row["item_description"] = node_description
|
|
19013
|
-
|
|
20104
|
+
row["item_path"] = []
|
|
20105
|
+
# We take the part of folder path which is inside the workspace
|
|
19014
20106
|
# as the item path:
|
|
19015
|
-
|
|
19016
|
-
|
|
19017
|
-
|
|
19018
|
-
|
|
19019
|
-
|
|
19020
|
-
|
|
20107
|
+
if (
|
|
20108
|
+
folder_path and workspace_name and workspace_name in folder_path
|
|
20109
|
+
): # check if folder_path is not empty, this can happy if document items are the workspace items
|
|
20110
|
+
try:
|
|
20111
|
+
# Item path are the list elements after the item that is the workspace name:
|
|
20112
|
+
row["item_path"] = folder_path[folder_path.index(workspace_name) + 1 :]
|
|
20113
|
+
except ValueError:
|
|
20114
|
+
self.logger.warning(
|
|
20115
|
+
"Cannot find workspace name -> '%s' in folder path -> %s while processing -> '%s' (%s)!",
|
|
20116
|
+
workspace_name,
|
|
20117
|
+
folder_path,
|
|
20118
|
+
node_name,
|
|
20119
|
+
node_id,
|
|
20120
|
+
)
|
|
19021
20121
|
row["item_download_name"] = str(node_id) if node_type == self.ITEM_TYPE_DOCUMENT else ""
|
|
19022
20122
|
row["item_mime_type"] = (
|
|
19023
20123
|
self.get_result_value(response=node, key="mime_type") if node_type == self.ITEM_TYPE_DOCUMENT else ""
|
|
@@ -19025,10 +20125,10 @@ class OTCS:
|
|
|
19025
20125
|
# URL specific data:
|
|
19026
20126
|
row["item_url"] = self.get_result_value(response=node, key="url") if node_type == self.ITEM_TYPE_URL else ""
|
|
19027
20127
|
if item_metadata and categories and categories["results"]:
|
|
19028
|
-
# Add columns for
|
|
20128
|
+
# Add columns for item node categories have been determined above.
|
|
19029
20129
|
self.add_attribute_columns(row=row, categories=categories, prefix="item_cat_")
|
|
19030
20130
|
|
|
19031
|
-
# Now we add the row to the Pandas Data Frame in the Data class:
|
|
20131
|
+
# Now we add the item row to the Pandas Data Frame in the Data class:
|
|
19032
20132
|
self.logger.info(
|
|
19033
20133
|
"Adding %s -> '%s' (%s) to data frame...",
|
|
19034
20134
|
"document" if node_type == self.ITEM_TYPE_DOCUMENT else "URL",
|
|
@@ -19038,7 +20138,9 @@ class OTCS:
|
|
|
19038
20138
|
with self._data.lock():
|
|
19039
20139
|
self._data.append(row)
|
|
19040
20140
|
|
|
19041
|
-
|
|
20141
|
+
# Success = True, Traverse = False
|
|
20142
|
+
# We have traverse = False because document or URL items have no sub-items.
|
|
20143
|
+
return (True, False)
|
|
19042
20144
|
|
|
19043
20145
|
# end check_node_item()
|
|
19044
20146
|
|
|
@@ -19076,19 +20178,37 @@ class OTCS:
|
|
|
19076
20178
|
"extract_zip": extract_zip,
|
|
19077
20179
|
}
|
|
19078
20180
|
|
|
20181
|
+
#
|
|
20182
|
+
# Define the list of executables to call for each node:
|
|
20183
|
+
#
|
|
20184
|
+
executables = []
|
|
20185
|
+
if workspaces:
|
|
20186
|
+
executables.append(check_node_workspace)
|
|
20187
|
+
if items:
|
|
20188
|
+
executables.append(check_node_item)
|
|
20189
|
+
if not executables:
|
|
20190
|
+
self.logger.error("Neither workspaces nor items are requested to be loaded. Nothing to do!")
|
|
20191
|
+
return None
|
|
20192
|
+
|
|
19079
20193
|
#
|
|
19080
20194
|
# Start the traversal of the nodes:
|
|
19081
20195
|
#
|
|
19082
20196
|
result = self.traverse_node_parallel(
|
|
19083
20197
|
node=node_id,
|
|
20198
|
+
# For each node we call these executables in this order to check if
|
|
20199
|
+
# the node should be added to the resulting data frame:
|
|
19084
20200
|
executables=[check_node_exclusions, check_node_workspace, check_node_item],
|
|
20201
|
+
workers=workers, # number of worker threads
|
|
19085
20202
|
exclude_node_ids=exclude_node_ids,
|
|
19086
20203
|
filter_workspace_data=filter_workspace_data,
|
|
19087
20204
|
filter_item_data=filter_item_data,
|
|
19088
20205
|
control_flags=control_flags,
|
|
19089
|
-
workers=workers, # number of worker threads
|
|
19090
20206
|
)
|
|
19091
20207
|
|
|
20208
|
+
# Wait for all download threads to complete:
|
|
20209
|
+
for thread in download_threads:
|
|
20210
|
+
thread.join()
|
|
20211
|
+
|
|
19092
20212
|
return result
|
|
19093
20213
|
|
|
19094
20214
|
# end method definition
|
|
@@ -19177,7 +20297,7 @@ class OTCS:
|
|
|
19177
20297
|
}
|
|
19178
20298
|
if message_override:
|
|
19179
20299
|
message.update(message_override)
|
|
19180
|
-
self.logger.
|
|
20300
|
+
self.logger.debug(
|
|
19181
20301
|
"Start Content Aviator embedding on -> '%s' (%s), type -> %s, crawl -> %s, wait for completion -> %s, workspaces -> %s, documents -> %s, images -> %s",
|
|
19182
20302
|
node_properties["name"],
|
|
19183
20303
|
node_properties["id"],
|
|
@@ -19188,7 +20308,7 @@ class OTCS:
|
|
|
19188
20308
|
document_metadata,
|
|
19189
20309
|
images,
|
|
19190
20310
|
)
|
|
19191
|
-
self.logger.debug("Sending WebSocket message -> %s", message)
|
|
20311
|
+
self.logger.debug("Sending WebSocket message -> %s...", message)
|
|
19192
20312
|
await websocket.send(message=json.dumps(message))
|
|
19193
20313
|
|
|
19194
20314
|
# Continuously listen for messages
|
|
@@ -19295,3 +20415,170 @@ class OTCS:
|
|
|
19295
20415
|
return success
|
|
19296
20416
|
|
|
19297
20417
|
# end method definition
|
|
20418
|
+
|
|
20419
|
+
def _get_document_template_raw(self, workspace_id: int) -> ET.Element | None:
|
|
20420
|
+
"""Get the raw template XML payload from a workspace.
|
|
20421
|
+
|
|
20422
|
+
Args:
|
|
20423
|
+
workspace_id (int):
|
|
20424
|
+
The ID of the workspace to generate the document from.
|
|
20425
|
+
|
|
20426
|
+
Returns:
|
|
20427
|
+
ET.Element | None:
|
|
20428
|
+
The XML Element with the payload to initiate a document generation, or None if an error occurred
|
|
20429
|
+
|
|
20430
|
+
"""
|
|
20431
|
+
|
|
20432
|
+
request_url = self.config()["csUrl"]
|
|
20433
|
+
|
|
20434
|
+
request_header = self.request_form_header()
|
|
20435
|
+
request_header["referer"] = "http://localhost"
|
|
20436
|
+
|
|
20437
|
+
data = {
|
|
20438
|
+
"func": "xecmpfdocgen.PowerDocsPayload",
|
|
20439
|
+
"wsId": str(workspace_id),
|
|
20440
|
+
"hideHeader": "true",
|
|
20441
|
+
"source": "CreateDocument",
|
|
20442
|
+
}
|
|
20443
|
+
|
|
20444
|
+
self.logger.debug(
|
|
20445
|
+
"Get document templates for workspace with ID -> %d; calling -> %s",
|
|
20446
|
+
workspace_id,
|
|
20447
|
+
request_url,
|
|
20448
|
+
)
|
|
20449
|
+
|
|
20450
|
+
response = self.do_request(
|
|
20451
|
+
url=request_url,
|
|
20452
|
+
method="POST",
|
|
20453
|
+
headers=request_header,
|
|
20454
|
+
data=data,
|
|
20455
|
+
timeout=None,
|
|
20456
|
+
failure_message="Failed to get document templates for workspace with ID -> {}".format(workspace_id),
|
|
20457
|
+
parse_request_response=False,
|
|
20458
|
+
)
|
|
20459
|
+
|
|
20460
|
+
if response is None:
|
|
20461
|
+
return None
|
|
20462
|
+
|
|
20463
|
+
try:
|
|
20464
|
+
text = response.text
|
|
20465
|
+
match = re.search(r'<textarea[^>]*name=["\']documentgeneration["\'][^>]*>(.*?)</textarea>', text, re.DOTALL)
|
|
20466
|
+
textarea_content = match.group(1).strip() if match else ""
|
|
20467
|
+
textarea_content = html.unescape(textarea_content)
|
|
20468
|
+
|
|
20469
|
+
# Load Payload into XML object
|
|
20470
|
+
# payload is an XML formatted string, load it into an XML object for further processing
|
|
20471
|
+
|
|
20472
|
+
root = ET.Element("documentgeneration", format="pdf")
|
|
20473
|
+
root.append(ET.fromstring(textarea_content))
|
|
20474
|
+
except (ET.ParseError, AttributeError) as exc:
|
|
20475
|
+
self.logger.error(
|
|
20476
|
+
"Cannot parse document template XML payload for workspace with ID -> %d! Error -> %s",
|
|
20477
|
+
workspace_id,
|
|
20478
|
+
exc,
|
|
20479
|
+
)
|
|
20480
|
+
return None
|
|
20481
|
+
else:
|
|
20482
|
+
return root
|
|
20483
|
+
|
|
20484
|
+
# end method definition
|
|
20485
|
+
|
|
20486
|
+
def get_document_template_names(self, workspace_id: int, root: ET.Element | None = None) -> list[str] | None:
|
|
20487
|
+
"""Get the list of available template names from a workspace.
|
|
20488
|
+
|
|
20489
|
+
Args:
|
|
20490
|
+
workspace_id (int):
|
|
20491
|
+
The ID of the workspace to generate the document from.
|
|
20492
|
+
root (ET.Element | None, optional):
|
|
20493
|
+
The XML Element with the payload to initiate a document generation.
|
|
20494
|
+
|
|
20495
|
+
Returns:
|
|
20496
|
+
list[str] | None:
|
|
20497
|
+
A list of template names available in the workspace, or None if an error occurred.
|
|
20498
|
+
|
|
20499
|
+
"""
|
|
20500
|
+
|
|
20501
|
+
if root is None:
|
|
20502
|
+
root = self._get_document_template_raw(workspace_id=workspace_id)
|
|
20503
|
+
if root is None:
|
|
20504
|
+
self.logger.error(
|
|
20505
|
+
"Cannot get document templates for workspace with ID -> %d",
|
|
20506
|
+
workspace_id,
|
|
20507
|
+
)
|
|
20508
|
+
return None
|
|
20509
|
+
template_names = [item.text for item in root.findall("startup/processing/templates/template")]
|
|
20510
|
+
|
|
20511
|
+
return template_names
|
|
20512
|
+
|
|
20513
|
+
# end method definition
|
|
20514
|
+
|
|
20515
|
+
def get_document_template(
|
|
20516
|
+
self, workspace_id: int, template_name: str, input_values: dict | None = None
|
|
20517
|
+
) -> str | None:
|
|
20518
|
+
"""Get the template XML payload from a workspace and a given template name.
|
|
20519
|
+
|
|
20520
|
+
Args:
|
|
20521
|
+
workspace_id (int):
|
|
20522
|
+
The ID of the workspace to generate the document from.
|
|
20523
|
+
template_name (str):
|
|
20524
|
+
The name of the template to use for document generation.
|
|
20525
|
+
input_values (dict | None, optional):
|
|
20526
|
+
A dictionary with input values to replace in the template.
|
|
20527
|
+
|
|
20528
|
+
Returns:
|
|
20529
|
+
str | None:
|
|
20530
|
+
The XML string with the payload to initiate a document generation, or None if an error occurred.
|
|
20531
|
+
|
|
20532
|
+
"""
|
|
20533
|
+
|
|
20534
|
+
root = self._get_document_template_raw(workspace_id=workspace_id)
|
|
20535
|
+
if root is None:
|
|
20536
|
+
self.logger.error(
|
|
20537
|
+
"Cannot get document template for workspace with ID -> %d",
|
|
20538
|
+
workspace_id,
|
|
20539
|
+
)
|
|
20540
|
+
return None
|
|
20541
|
+
|
|
20542
|
+
template_names = self.get_document_template_names(workspace_id=workspace_id, root=root)
|
|
20543
|
+
|
|
20544
|
+
if template_name not in template_names:
|
|
20545
|
+
self.logger.error(
|
|
20546
|
+
"Template name -> '%s' not found in workspace with ID -> %d! Available templates are: %s",
|
|
20547
|
+
template_name,
|
|
20548
|
+
workspace_id,
|
|
20549
|
+
", ".join(template_names),
|
|
20550
|
+
)
|
|
20551
|
+
return None
|
|
20552
|
+
|
|
20553
|
+
# remove startup/processing
|
|
20554
|
+
startup = root.find("startup")
|
|
20555
|
+
# Find the existing application element and update its sysid attribute
|
|
20556
|
+
application = startup.find("application")
|
|
20557
|
+
application.set("sysid", "3adfd3a4-718f-4b9c-ac93-72efbcdf17f1")
|
|
20558
|
+
|
|
20559
|
+
processing = startup.find("processing")
|
|
20560
|
+
|
|
20561
|
+
# Clear processing information
|
|
20562
|
+
processing.clear()
|
|
20563
|
+
|
|
20564
|
+
modus = ET.SubElement(processing, "modus")
|
|
20565
|
+
modus.text = "local"
|
|
20566
|
+
editor = ET.SubElement(processing, "editor")
|
|
20567
|
+
editor.text = "false"
|
|
20568
|
+
template = ET.SubElement(processing, "template", type="Name")
|
|
20569
|
+
template.text = template_name
|
|
20570
|
+
channel = ET.SubElement(processing, "channel")
|
|
20571
|
+
channel.text = "save"
|
|
20572
|
+
|
|
20573
|
+
# Add static query information for userId and asOfDate
|
|
20574
|
+
if input_values:
|
|
20575
|
+
query = ET.SubElement(startup, "query", type="value")
|
|
20576
|
+
input_element = ET.SubElement(query, "input")
|
|
20577
|
+
|
|
20578
|
+
for column, value in input_values.items():
|
|
20579
|
+
value_element = ET.SubElement(input_element, "value", column=column)
|
|
20580
|
+
value_element.text = value
|
|
20581
|
+
|
|
20582
|
+
payload = ET.tostring(root, encoding="utf8").decode("utf8")
|
|
20583
|
+
|
|
20584
|
+
return payload
|