pyxecm 0.0.18__py3-none-any.whl → 0.0.19__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/__init__.py +10 -10
- pyxecm/assoc.py +139 -0
- pyxecm/m365.py +11 -4
- pyxecm/main.py +28 -22
- pyxecm/otcs.py +79 -73
- pyxecm/otds.py +372 -77
- pyxecm/payload.py +1683 -382
- pyxecm/xml.py +436 -0
- {pyxecm-0.0.18.dist-info → pyxecm-0.0.19.dist-info}/METADATA +4 -5
- pyxecm-0.0.19.dist-info/RECORD +20 -0
- pyxecm/llm.py +0 -451
- pyxecm-0.0.18.dist-info/RECORD +0 -19
- {pyxecm-0.0.18.dist-info → pyxecm-0.0.19.dist-info}/LICENSE +0 -0
- {pyxecm-0.0.18.dist-info → pyxecm-0.0.19.dist-info}/WHEEL +0 -0
- {pyxecm-0.0.18.dist-info → pyxecm-0.0.19.dist-info}/top_level.txt +0 -0
pyxecm/__init__.py
CHANGED
|
@@ -2,16 +2,16 @@ import logging
|
|
|
2
2
|
import os
|
|
3
3
|
|
|
4
4
|
# pyxecm packages
|
|
5
|
-
from
|
|
6
|
-
from
|
|
7
|
-
from
|
|
8
|
-
from
|
|
9
|
-
from
|
|
10
|
-
from
|
|
11
|
-
from
|
|
12
|
-
from
|
|
13
|
-
from
|
|
14
|
-
from
|
|
5
|
+
from .main import *
|
|
6
|
+
from .k8s import *
|
|
7
|
+
from .otac import *
|
|
8
|
+
from .otcs import *
|
|
9
|
+
from .otds import *
|
|
10
|
+
from .otiv import *
|
|
11
|
+
from .otpd import *
|
|
12
|
+
from .payload import *
|
|
13
|
+
from .translate import *
|
|
14
|
+
from .web import *
|
|
15
15
|
|
|
16
16
|
logging.basicConfig(
|
|
17
17
|
format="%(asctime)s %(levelname)s [%(name)s] %(message)s",
|
pyxecm/assoc.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Extended ECM Assoc Module to implement functions to read / write from
|
|
3
|
+
so called "Assoc" data structures in Extended ECM. Right now this module
|
|
4
|
+
is used to tweak settings in XML-based transport packages that include
|
|
5
|
+
Assoc structures inside some of the XML elements.
|
|
6
|
+
|
|
7
|
+
Class: Assoc
|
|
8
|
+
Methods:
|
|
9
|
+
|
|
10
|
+
stringToDict: convert an Assoc string to an Python dict representing the assoc values
|
|
11
|
+
dictToString: converting an Assoc dict to an Assoc string
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
__author__ = "Dr. Marc Diefenbruch"
|
|
15
|
+
__copyright__ = "Copyright 2023, OpenText"
|
|
16
|
+
__credits__ = ["Kai-Philip Gatzweiler"]
|
|
17
|
+
__maintainer__ = "Dr. Marc Diefenbruch"
|
|
18
|
+
__email__ = "mdiefenb@opentext.com"
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
import html
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Assoc:
|
|
25
|
+
@classmethod
|
|
26
|
+
def isUnicodeEscaped(cls, assoc_string: str) -> bool:
|
|
27
|
+
pattern = r"\\u[0-9a-fA-F]{4}"
|
|
28
|
+
matches = re.findall(pattern, assoc_string)
|
|
29
|
+
return len(matches) > 0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def escapeUnicode(cls, assoc_string: str) -> str:
|
|
34
|
+
encoded_string = assoc_string.encode('unicode_escape') # .decode()
|
|
35
|
+
|
|
36
|
+
return encoded_string
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def unescapeUnicode(cls, assoc_string: str) -> str:
|
|
41
|
+
try:
|
|
42
|
+
decoded_string = bytes(assoc_string, "utf-8").decode("unicode_escape")
|
|
43
|
+
return decoded_string
|
|
44
|
+
except UnicodeDecodeError:
|
|
45
|
+
return assoc_string
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def isHTMLEscaped(cls, assoc_string: str) -> bool:
|
|
49
|
+
decoded_string = html.unescape(assoc_string)
|
|
50
|
+
return assoc_string != decoded_string
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def unescapeHTML(cls, assoc_string: str) -> str:
|
|
54
|
+
decoded_string = html.unescape(assoc_string)
|
|
55
|
+
return decoded_string
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def stringToDict(cls, assoc_string: str) -> dict:
|
|
59
|
+
if cls.isHTMLEscaped(assoc_string):
|
|
60
|
+
assoc_string = cls.unescapeHTML(assoc_string)
|
|
61
|
+
if cls.isUnicodeEscaped(assoc_string):
|
|
62
|
+
assoc_string = cls.unescapeUnicode(assoc_string)
|
|
63
|
+
|
|
64
|
+
# Split the string using regex pattern
|
|
65
|
+
pieces = re.split(r",(?=(?:[^']*'[^']*')*[^']*$)", assoc_string)
|
|
66
|
+
|
|
67
|
+
# Trim any leading/trailing spaces from each piece
|
|
68
|
+
pieces = [piece.strip() for piece in pieces]
|
|
69
|
+
|
|
70
|
+
# Split the last pieces from the assoc close tag
|
|
71
|
+
last_piece = pieces[-1].split(">")[0]
|
|
72
|
+
|
|
73
|
+
# Remove the first two and last pieces from the list
|
|
74
|
+
# the first two are mostly "1" and "?"
|
|
75
|
+
pieces = pieces[2:-1]
|
|
76
|
+
|
|
77
|
+
# Insert the last pieces separately
|
|
78
|
+
pieces.append(last_piece)
|
|
79
|
+
|
|
80
|
+
assoc_dict: dict = {}
|
|
81
|
+
|
|
82
|
+
for piece in pieces:
|
|
83
|
+
name = piece.split("=")[0]
|
|
84
|
+
if name[0] == "'":
|
|
85
|
+
name = name[1:]
|
|
86
|
+
if name[-1] == "'":
|
|
87
|
+
name = name[:-1]
|
|
88
|
+
value = piece.split("=")[1]
|
|
89
|
+
if value[0] == "'":
|
|
90
|
+
value = value[1:]
|
|
91
|
+
if value[-1] == "'":
|
|
92
|
+
value = value[:-1]
|
|
93
|
+
assoc_dict[name] = value
|
|
94
|
+
|
|
95
|
+
return assoc_dict
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def dictToString(cls, assoc_dict: dict) -> str:
|
|
99
|
+
assoc_string: str = "A<1,?,"
|
|
100
|
+
|
|
101
|
+
for item in assoc_dict.items():
|
|
102
|
+
assoc_string += "\u0027" + item[0] + "\u0027"
|
|
103
|
+
assoc_string += "="
|
|
104
|
+
# Extended ECM's XML is a bit special in cases.
|
|
105
|
+
# If the value is empty set (curly braces) it does
|
|
106
|
+
# not put it in quotes. As Extended ECM is also very
|
|
107
|
+
# picky about XML syntax we better produce it exactly like that.
|
|
108
|
+
if item[1] == "{}":
|
|
109
|
+
assoc_string += item[1] + ","
|
|
110
|
+
else:
|
|
111
|
+
assoc_string += "\u0027" + item[1] + "\u0027,"
|
|
112
|
+
|
|
113
|
+
if assoc_dict.items():
|
|
114
|
+
assoc_string = assoc_string[:-1]
|
|
115
|
+
assoc_string += ">"
|
|
116
|
+
return assoc_string
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def extractSubstring(
|
|
120
|
+
cls, input_string: str, start_sequence: str, stop_sequence: str
|
|
121
|
+
):
|
|
122
|
+
start_index = input_string.find(start_sequence)
|
|
123
|
+
if start_index == -1:
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
end_index = input_string.find(stop_sequence, start_index)
|
|
127
|
+
if end_index == -1:
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
end_index += len(stop_sequence)
|
|
131
|
+
return input_string[start_index:end_index]
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def extractAssocString(cls, input_string: str, is_escaped: bool = False) -> str:
|
|
135
|
+
if is_escaped:
|
|
136
|
+
assoc_string = cls.extractSubstring(input_string, "A<", ">")
|
|
137
|
+
else:
|
|
138
|
+
assoc_string = cls.extractSubstring(input_string, "A<", ">")
|
|
139
|
+
return assoc_string
|
pyxecm/m365.py
CHANGED
|
@@ -200,7 +200,7 @@ class M365(object):
|
|
|
200
200
|
response_object (object): this is reponse object delivered by the request call
|
|
201
201
|
additional_error_message (string, optional): use a more specific error message
|
|
202
202
|
in case of an error
|
|
203
|
-
show_error (boolean): True: write an error to the log file
|
|
203
|
+
show_error (boolean): True: write an error to the log file
|
|
204
204
|
False: write a warning to the log file
|
|
205
205
|
Returns:
|
|
206
206
|
dictionary: response information or None in case of an error
|
|
@@ -496,7 +496,7 @@ class M365(object):
|
|
|
496
496
|
|
|
497
497
|
# end method definition
|
|
498
498
|
|
|
499
|
-
def getGroups(self):
|
|
499
|
+
def getGroups(self, max_number: int = 250):
|
|
500
500
|
"""Get list all all groups in M365 tenant
|
|
501
501
|
|
|
502
502
|
Args:
|
|
@@ -512,7 +512,9 @@ class M365(object):
|
|
|
512
512
|
|
|
513
513
|
retries = 0
|
|
514
514
|
while True:
|
|
515
|
-
response = requests.get(
|
|
515
|
+
response = requests.get(
|
|
516
|
+
request_url, headers=request_header, params={"$top": str(max_number)}
|
|
517
|
+
)
|
|
516
518
|
if response.ok:
|
|
517
519
|
return self.parseRequestResponse(response)
|
|
518
520
|
# Check if Session has expired - then re-authenticate and try once more
|
|
@@ -990,10 +992,15 @@ class M365(object):
|
|
|
990
992
|
"""
|
|
991
993
|
|
|
992
994
|
# Get list of all existing M365 groups/teams:
|
|
993
|
-
response = self.getGroups()
|
|
995
|
+
response = self.getGroups(max_number=500)
|
|
994
996
|
if not "value" in response or not response["value"]:
|
|
995
997
|
return False
|
|
996
998
|
groups = response["value"]
|
|
999
|
+
logger.info(
|
|
1000
|
+
"Found -> {} existing M365 groups. Checking which ones should be deleted...".format(
|
|
1001
|
+
len(groups)
|
|
1002
|
+
)
|
|
1003
|
+
)
|
|
997
1004
|
|
|
998
1005
|
# Process all groups and check if the< should be
|
|
999
1006
|
# deleted:
|
pyxecm/main.py
CHANGED
|
@@ -53,8 +53,6 @@ otcs_replicas_backend = 0
|
|
|
53
53
|
|
|
54
54
|
# global Logging LEVEL environment variable
|
|
55
55
|
# This produces debug-level output in pod logging
|
|
56
|
-
# and also activates stop_on_error parameter
|
|
57
|
-
# in processTransportPackages()
|
|
58
56
|
LOGLEVEL = os.environ.get("LOGLEVEL", "INFO")
|
|
59
57
|
|
|
60
58
|
# The following CUST artifacts are created by the main.tf in the python module:
|
|
@@ -95,6 +93,7 @@ OTCS_PUBLIC_URL = os.environ.get("OTCS_PUBLIC_URL", "otcs.xecm.dev")
|
|
|
95
93
|
OTCS_PORT = OTCS_PORT_BACKEND = os.environ.get("OTCS_SERVICE_PORT_OTCS", 8080)
|
|
96
94
|
OTCS_PORT_FRONTEND = 80
|
|
97
95
|
OTCS_ADMIN = os.environ.get("OTCS_ADMIN", "admin")
|
|
96
|
+
OTCS_PASSWORD = os.environ.get("OTCS_PASSWORD", "Opentext1!")
|
|
98
97
|
OTCS_PARTITION = os.environ.get("OTCS_PARTITION", "Content Server Members")
|
|
99
98
|
OTCS_RESOURCE_NAME = "cs"
|
|
100
99
|
OTCS_K8S_STATEFUL_SET_FRONTEND = "otcs-frontend"
|
|
@@ -108,7 +107,6 @@ OTCS_LICENSE_FEATURE = "X3"
|
|
|
108
107
|
# K8s service name for maintenance pod
|
|
109
108
|
OTCS_MAINTENANCE_SERVICE_NAME = "maintenance"
|
|
110
109
|
OTCS_MAINTENANCE_SERVICE_PORT = 80 # K8s service name for maintenance pod
|
|
111
|
-
OTCS_PASSWORD = os.environ.get("OTCS_PASSWORD", "Opentext1!")
|
|
112
110
|
|
|
113
111
|
# Archive Center constants:
|
|
114
112
|
OTAC_ENABLED = os.environ.get("OTAC_ENABLED", True)
|
|
@@ -476,6 +474,9 @@ def initOTCS(
|
|
|
476
474
|
|
|
477
475
|
otds_object.updateResource(name="cs", resource=otcs_resource)
|
|
478
476
|
|
|
477
|
+
# Allow impersonation of the resource for all users:
|
|
478
|
+
otds_object.impersonateResource(resource_name)
|
|
479
|
+
|
|
479
480
|
return otcs_object
|
|
480
481
|
|
|
481
482
|
# end function definition
|
|
@@ -835,7 +836,7 @@ def initOTAWP(otds_object: object, k8s_object: object):
|
|
|
835
836
|
|
|
836
837
|
logger.info("OTDS resource ID for AppWorks Platform -> {}".format(awp_resource_id))
|
|
837
838
|
|
|
838
|
-
placeholder_values["
|
|
839
|
+
placeholder_values["OTAWP_RESOURCE_ID"] = str(awp_resource_id)
|
|
839
840
|
|
|
840
841
|
logger.debug("Placeholder values after OTAWP init = {}".format(placeholder_values))
|
|
841
842
|
|
|
@@ -933,6 +934,8 @@ def restartOTCSPods(otcs_object: object, k8s_object: object):
|
|
|
933
934
|
global otcs_replicas_frontend
|
|
934
935
|
global otcs_replicas_backend
|
|
935
936
|
|
|
937
|
+
logger.info("Restart OTCS frontend and backend pods...")
|
|
938
|
+
|
|
936
939
|
# Restart all frontends:
|
|
937
940
|
for x in range(0, otcs_replicas_frontend):
|
|
938
941
|
pod_name = OTCS_K8S_STATEFUL_SET_FRONTEND + "-" + str(x)
|
|
@@ -982,6 +985,8 @@ def restartOTCSPods(otcs_object: object, k8s_object: object):
|
|
|
982
985
|
logger.info("Reactivate Liveness probe for pod -> {}".format(pod_name))
|
|
983
986
|
k8s_object.execPodCommand(pod_name, ["/bin/sh", "-c", "rm /tmp/keepalive"])
|
|
984
987
|
|
|
988
|
+
logger.info("Restart OTCS frontend and backend pods has been completed.")
|
|
989
|
+
|
|
985
990
|
# end function definition
|
|
986
991
|
|
|
987
992
|
|
|
@@ -991,7 +996,7 @@ def restartOTACPod(k8s_object: object) -> bool:
|
|
|
991
996
|
Args:
|
|
992
997
|
k8s_object (object): Kubernetes object
|
|
993
998
|
Returns:
|
|
994
|
-
boolean: True if restart was done, False if error occured
|
|
999
|
+
boolean: True if restart was done, False if error occured
|
|
995
1000
|
"""
|
|
996
1001
|
|
|
997
1002
|
if not OTAC_ENABLED:
|
|
@@ -1135,7 +1140,7 @@ def customization_run():
|
|
|
1135
1140
|
# Configure required OTDS resources as AppWorks doesn't do this on its own:
|
|
1136
1141
|
initOTAWP(otds_object, k8s_object)
|
|
1137
1142
|
else:
|
|
1138
|
-
placeholder_values["
|
|
1143
|
+
placeholder_values["OTAWP_RESOURCE_ID"] = ""
|
|
1139
1144
|
|
|
1140
1145
|
if O365_ENABLED == "true": # is M365 enabled?
|
|
1141
1146
|
logger.info("======== Initialize MS Graph API ========")
|
|
@@ -1251,17 +1256,18 @@ def customization_run():
|
|
|
1251
1256
|
totalStartTime = datetime.now()
|
|
1252
1257
|
|
|
1253
1258
|
payload_object = payload.Payload(
|
|
1254
|
-
cust_payload,
|
|
1255
|
-
CUST_SETTINGS_DIR,
|
|
1256
|
-
k8s_object,
|
|
1257
|
-
otds_object,
|
|
1258
|
-
otac_object,
|
|
1259
|
-
otcs_backend_object,
|
|
1260
|
-
otcs_frontend_object,
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1259
|
+
payload_source=cust_payload,
|
|
1260
|
+
custom_settings_dir=CUST_SETTINGS_DIR,
|
|
1261
|
+
k8s_object=k8s_object,
|
|
1262
|
+
otds_object=otds_object,
|
|
1263
|
+
otac_object=otac_object,
|
|
1264
|
+
otcs_backend_object=otcs_backend_object,
|
|
1265
|
+
otcs_frontend_object=otcs_frontend_object,
|
|
1266
|
+
otcs_restart_callback=restartOTCSPods,
|
|
1267
|
+
otiv_object=otiv_object,
|
|
1268
|
+
m365_object=m365_object,
|
|
1269
|
+
placeholder_values=placeholder_values, # this dict includes placeholder replacements for the Ressource IDs of OTAWP and OTCS
|
|
1270
|
+
stop_on_error=True if LOGLEVEL == "DEBUG" else False,
|
|
1265
1271
|
)
|
|
1266
1272
|
# Load the payload file and initialize the payload sections:
|
|
1267
1273
|
if not payload_object.initPayload():
|
|
@@ -1332,11 +1338,11 @@ def customization_run():
|
|
|
1332
1338
|
logger.info("OTCS frontend is now back in Production Mode!")
|
|
1333
1339
|
|
|
1334
1340
|
# Restart OTCS frontend and backend pods:
|
|
1335
|
-
logger.info("Restart OTCS frontend and backend pods...")
|
|
1336
|
-
restartOTCSPods(otcs_backend_object, k8s_object)
|
|
1337
|
-
# give some additional time to make sure service is responsive
|
|
1338
|
-
time.sleep(30)
|
|
1339
|
-
logger.info("Restart OTCS frontend and backend pods has been completed.")
|
|
1341
|
+
# logger.info("Restart OTCS frontend and backend pods...")
|
|
1342
|
+
# restartOTCSPods(otcs_backend_object, k8s_object)
|
|
1343
|
+
# # give some additional time to make sure service is responsive
|
|
1344
|
+
# time.sleep(30)
|
|
1345
|
+
# logger.info("Restart OTCS frontend and backend pods has been completed.")
|
|
1340
1346
|
|
|
1341
1347
|
# Restart AppWorksPlatform pod if it is deployed (to make settings effective):
|
|
1342
1348
|
if OTAWP_ENABLED == "true": # is AppWorks Platform deployed?
|
pyxecm/otcs.py
CHANGED
|
@@ -74,8 +74,6 @@ unpackTransportPackage: Unpack an existing Transport Package into an existing Wo
|
|
|
74
74
|
deployWorkbench: Deploy an existing Workbench
|
|
75
75
|
deployTransport: Main method to deploy a transport. This uses subfunctions to upload,
|
|
76
76
|
unpackage and deploy the transport, and creates the required workbench
|
|
77
|
-
replaceInXmlFiles: Replace all occurrences of the search pattern with the replace string in all
|
|
78
|
-
XML files in the directory and its subdirectories.
|
|
79
77
|
replaceTransportPlaceholders: Search and replace strings in the XML files of the transport packlage
|
|
80
78
|
|
|
81
79
|
getWorkspaceTypes: Get all workspace types configured in Extended ECM
|
|
@@ -154,7 +152,7 @@ import json
|
|
|
154
152
|
import urllib.parse
|
|
155
153
|
from datetime import datetime
|
|
156
154
|
import zipfile
|
|
157
|
-
import
|
|
155
|
+
from pyxecm.xml import *
|
|
158
156
|
|
|
159
157
|
logger = logging.getLogger(os.path.basename(__file__))
|
|
160
158
|
|
|
@@ -1706,7 +1704,11 @@ class OTCS(object):
|
|
|
1706
1704
|
# end method definition
|
|
1707
1705
|
|
|
1708
1706
|
def getNodeByParentAndName(
|
|
1709
|
-
self,
|
|
1707
|
+
self,
|
|
1708
|
+
parent_id: int,
|
|
1709
|
+
name: str,
|
|
1710
|
+
fields: str = "properties",
|
|
1711
|
+
show_error: bool = False,
|
|
1710
1712
|
) -> dict:
|
|
1711
1713
|
"""Get a node based on the parent ID and name. This method does basically
|
|
1712
1714
|
a query with "where_name" and the "result" is a list.
|
|
@@ -1714,6 +1716,7 @@ class OTCS(object):
|
|
|
1714
1716
|
Args:
|
|
1715
1717
|
parent_id (integer) is the node Id of the parent node
|
|
1716
1718
|
name (string) is the name of the node to get
|
|
1719
|
+
fields (string): which fields to retrieve. This can have a big impact on performance!
|
|
1717
1720
|
show_error (boolean, optional): treat as error if node is not found
|
|
1718
1721
|
Returns:
|
|
1719
1722
|
dictionary: Node information or None if no node with this name is found in parent.
|
|
@@ -1722,6 +1725,8 @@ class OTCS(object):
|
|
|
1722
1725
|
|
|
1723
1726
|
# Add query parameters (these are NOT passed via JSon body!)
|
|
1724
1727
|
query = {"where_name": name}
|
|
1728
|
+
if fields:
|
|
1729
|
+
query["fields"] = fields
|
|
1725
1730
|
encoded_query = urllib.parse.urlencode(query, doseq=True)
|
|
1726
1731
|
|
|
1727
1732
|
request_url = (
|
|
@@ -1809,7 +1814,7 @@ class OTCS(object):
|
|
|
1809
1814
|
|
|
1810
1815
|
# end method definition
|
|
1811
1816
|
|
|
1812
|
-
def getNodeByVolumeAndPath(self, volume_type: int, path: list) -> dict:
|
|
1817
|
+
def getNodeByVolumeAndPath(self, volume_type: int, path: list = []) -> dict:
|
|
1813
1818
|
"""Get a node based on the volume and path (list of container items).
|
|
1814
1819
|
|
|
1815
1820
|
Args:
|
|
@@ -1830,6 +1835,7 @@ class OTCS(object):
|
|
|
1830
1835
|
"Physical Objects Workspace" = 413
|
|
1831
1836
|
"Extended ECM" = 882
|
|
1832
1837
|
"Enterprise Workspace" = 141
|
|
1838
|
+
"Personal Workspace" = 142
|
|
1833
1839
|
"Business Workspaces" = 862
|
|
1834
1840
|
path (list): list of container items (top down), last item is name of to be retrieved item.
|
|
1835
1841
|
If path is empty the node of the volume is returned.
|
|
@@ -1918,6 +1924,7 @@ class OTCS(object):
|
|
|
1918
1924
|
show_hidden: bool = False,
|
|
1919
1925
|
limit: int = 100,
|
|
1920
1926
|
page: int = 1,
|
|
1927
|
+
fields: str = "properties", # per default we just get the most important information
|
|
1921
1928
|
) -> dict:
|
|
1922
1929
|
"""Get a subnodes of a parent node ID.
|
|
1923
1930
|
|
|
@@ -1931,6 +1938,7 @@ class OTCS(object):
|
|
|
1931
1938
|
show_hidden (boolean, optional): list also hidden items (default = False)
|
|
1932
1939
|
limit (integer, optional): maximum number of results (default = 100)
|
|
1933
1940
|
page (integer, optional): number of result page (default = 1 = 1st page)
|
|
1941
|
+
fields (string): which fields to retrieve. This can have a big impact on performance!
|
|
1934
1942
|
Returns:
|
|
1935
1943
|
dictionary: Subnodes information or None if no node with this parent ID is found.
|
|
1936
1944
|
"""
|
|
@@ -1946,6 +1954,8 @@ class OTCS(object):
|
|
|
1946
1954
|
query["show_hidden"] = show_hidden
|
|
1947
1955
|
if page > 1:
|
|
1948
1956
|
query["page"] = page
|
|
1957
|
+
if fields:
|
|
1958
|
+
query["fields"] = fields
|
|
1949
1959
|
|
|
1950
1960
|
encodedQuery = urllib.parse.urlencode(query, doseq=True)
|
|
1951
1961
|
|
|
@@ -2574,7 +2584,11 @@ class OTCS(object):
|
|
|
2574
2584
|
if not version_number:
|
|
2575
2585
|
response = self.getLatestDocumentVersion(node_id)
|
|
2576
2586
|
if not response:
|
|
2577
|
-
logger.error(
|
|
2587
|
+
logger.error(
|
|
2588
|
+
"Cannot get latest version of document with ID -> {}".format(
|
|
2589
|
+
node_id
|
|
2590
|
+
)
|
|
2591
|
+
)
|
|
2578
2592
|
version_number = response["data"]["version_number"]
|
|
2579
2593
|
|
|
2580
2594
|
request_url = (
|
|
@@ -2583,8 +2597,7 @@ class OTCS(object):
|
|
|
2583
2597
|
+ str(node_id)
|
|
2584
2598
|
+ "/versions/"
|
|
2585
2599
|
+ str(version_number)
|
|
2586
|
-
+ "/content
|
|
2587
|
-
+ str(node_id)
|
|
2600
|
+
+ "/content"
|
|
2588
2601
|
)
|
|
2589
2602
|
request_header = self.requestDownloadHeader()
|
|
2590
2603
|
|
|
@@ -3194,7 +3207,7 @@ class OTCS(object):
|
|
|
3194
3207
|
logger.info("Deploy workbench -> {} ({})".format(workbench_name, workbench_id))
|
|
3195
3208
|
response = self.deployWorkbench(workbench_id)
|
|
3196
3209
|
if response == None:
|
|
3197
|
-
logger.
|
|
3210
|
+
logger.error("Failed to deploy workbench -> {}".format(workbench_name))
|
|
3198
3211
|
return None
|
|
3199
3212
|
|
|
3200
3213
|
logger.info(
|
|
@@ -3212,52 +3225,6 @@ class OTCS(object):
|
|
|
3212
3225
|
|
|
3213
3226
|
# end method definition
|
|
3214
3227
|
|
|
3215
|
-
def replaceInXmlFiles(
|
|
3216
|
-
self, directory: str, search_pattern: str, replace_string: str
|
|
3217
|
-
) -> bool:
|
|
3218
|
-
"""Replaces all occurrences of the search pattern with the replace string in all XML files
|
|
3219
|
-
in the directory and its subdirectories.
|
|
3220
|
-
|
|
3221
|
-
Args:
|
|
3222
|
-
directory (string): directory to traverse for XML files
|
|
3223
|
-
search_pattern (sting): string to search in the XML file
|
|
3224
|
-
replace_string (string): replacement string
|
|
3225
|
-
Returns:
|
|
3226
|
-
boolean: True if a replacement happened, False otherwise
|
|
3227
|
-
"""
|
|
3228
|
-
# Define the regular expression pattern to search for
|
|
3229
|
-
pattern = re.compile(search_pattern)
|
|
3230
|
-
found = False
|
|
3231
|
-
|
|
3232
|
-
# Traverse the directory and its subdirectories
|
|
3233
|
-
for subdir, dirs, files in os.walk(directory):
|
|
3234
|
-
for file in files:
|
|
3235
|
-
# Check if the file is an XML file
|
|
3236
|
-
if file.endswith(".xml"):
|
|
3237
|
-
# Read the contents of the file
|
|
3238
|
-
file_path = os.path.join(subdir, file)
|
|
3239
|
-
with open(file_path, "r") as f:
|
|
3240
|
-
contents = f.read()
|
|
3241
|
-
|
|
3242
|
-
# Replace all occurrences of the search pattern with the replace string
|
|
3243
|
-
new_contents = pattern.sub(replace_string, contents)
|
|
3244
|
-
|
|
3245
|
-
# Write the updated contents to the file if there were replacements
|
|
3246
|
-
if contents != new_contents:
|
|
3247
|
-
logger.info(
|
|
3248
|
-
"Found search string -> {} in XML file -> {}. Updating content...".format(
|
|
3249
|
-
search_pattern, file_path
|
|
3250
|
-
)
|
|
3251
|
-
)
|
|
3252
|
-
# Write the updated contents to the file
|
|
3253
|
-
with open(file_path, "w") as f:
|
|
3254
|
-
f.write(new_contents)
|
|
3255
|
-
found = True
|
|
3256
|
-
|
|
3257
|
-
return found
|
|
3258
|
-
|
|
3259
|
-
# end method definition
|
|
3260
|
-
|
|
3261
3228
|
def replaceTransportPlaceholders(
|
|
3262
3229
|
self, zip_file_path: str, replacements: list
|
|
3263
3230
|
) -> bool:
|
|
@@ -3285,38 +3252,77 @@ class OTCS(object):
|
|
|
3285
3252
|
|
|
3286
3253
|
# Replace search pattern with replace string in all XML files in the directory and its subdirectories
|
|
3287
3254
|
for replacement in replacements:
|
|
3288
|
-
if
|
|
3255
|
+
if not "value" in replacement:
|
|
3256
|
+
logger.error("Replacement needs a value but it is not specified. Skipping...")
|
|
3257
|
+
continue
|
|
3258
|
+
if "enabled" in replacement and not replacement["enabled"]:
|
|
3289
3259
|
logger.info(
|
|
3290
|
-
"
|
|
3291
|
-
replacement["value"]
|
|
3292
|
-
)
|
|
3260
|
+
"Replacement for transport -> {} is disabled. Skipping...".format(zip_file_path)
|
|
3293
3261
|
)
|
|
3294
3262
|
continue
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3263
|
+
# there are two types of replacements:
|
|
3264
|
+
# 1. XPath - more elegant and powerful
|
|
3265
|
+
# 2. Search & Replace - basically treat the XML file like a like file and do a search & replace
|
|
3266
|
+
if "xpath" in replacement:
|
|
3267
|
+
logger.info(
|
|
3268
|
+
"Using xpath -> {} to narrow down the replacement".format(
|
|
3269
|
+
replacement["xpath"]
|
|
3270
|
+
)
|
|
3298
3271
|
)
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3272
|
+
if "setting" in replacement:
|
|
3273
|
+
logger.info(
|
|
3274
|
+
"Looking up setting -> {} in XML element".format(
|
|
3275
|
+
replacement["setting"]
|
|
3276
|
+
)
|
|
3277
|
+
)
|
|
3278
|
+
if "assoc_elem" in replacement:
|
|
3279
|
+
logger.info(
|
|
3280
|
+
"Looking up assoc element -> {} in XML element".format(
|
|
3281
|
+
replacement["assoc_elem"]
|
|
3282
|
+
)
|
|
3283
|
+
)
|
|
3284
|
+
else: # we have a simple "search & replace" replacement
|
|
3285
|
+
if not "placeholder" in replacement:
|
|
3286
|
+
logger.error("Replacement without an xpath needs a placeholder value but it is not specified. Skipping...")
|
|
3287
|
+
continue
|
|
3288
|
+
if replacement.get("placeholder") == replacement["value"]:
|
|
3289
|
+
logger.info(
|
|
3290
|
+
"Placeholder and replacement are identical -> {}. Skipping...".format(
|
|
3291
|
+
replacement["value"]
|
|
3292
|
+
)
|
|
3293
|
+
)
|
|
3294
|
+
continue
|
|
3295
|
+
logger.info(
|
|
3296
|
+
"Replace -> {} with -> {} in Transport package -> {}".format(
|
|
3297
|
+
replacement["placeholder"], replacement["value"], zip_file_folder
|
|
3298
|
+
)
|
|
3299
|
+
)
|
|
3300
|
+
|
|
3301
|
+
found = XML.replaceInXmlFiles(
|
|
3302
|
+
zip_file_folder,
|
|
3303
|
+
replacement.get("placeholder"),
|
|
3304
|
+
replacement["value"],
|
|
3305
|
+
replacement.get("xpath"),
|
|
3306
|
+
replacement.get("setting"),
|
|
3307
|
+
replacement.get("assoc_elem"),
|
|
3302
3308
|
)
|
|
3303
3309
|
if found:
|
|
3304
3310
|
logger.info(
|
|
3305
|
-
"
|
|
3306
|
-
replacement
|
|
3311
|
+
"Replacement -> {} has been completed successfully for Transport package -> {}".format(
|
|
3312
|
+
replacement, zip_file_folder
|
|
3307
3313
|
)
|
|
3308
3314
|
)
|
|
3309
3315
|
modified = True
|
|
3310
3316
|
else:
|
|
3311
3317
|
logger.warning(
|
|
3312
|
-
"
|
|
3313
|
-
replacement
|
|
3318
|
+
"Replacement -> {} failed for Transport package -> {}".format(
|
|
3319
|
+
replacement, zip_file_folder
|
|
3314
3320
|
)
|
|
3315
3321
|
)
|
|
3316
3322
|
|
|
3317
3323
|
if not modified:
|
|
3318
3324
|
logger.warning(
|
|
3319
|
-
"None of the replacements have been
|
|
3325
|
+
"None of the specified replacements have been successful for Transport package -> {}. No need to create a new transport package.".format(
|
|
3320
3326
|
zip_file_folder
|
|
3321
3327
|
)
|
|
3322
3328
|
)
|
|
@@ -4159,13 +4165,13 @@ class OTCS(object):
|
|
|
4159
4165
|
if not os.path.exists(file_path):
|
|
4160
4166
|
logger.error("Workdpace icon file does not exist -> {}".format(file_path))
|
|
4161
4167
|
return None
|
|
4162
|
-
|
|
4163
|
-
# icon_file = open(file_path, "rb")
|
|
4168
|
+
|
|
4169
|
+
# icon_file = open(file_path, "rb")
|
|
4164
4170
|
|
|
4165
4171
|
updateWorkspaceIconPutBody = {
|
|
4166
4172
|
"file_content_type": file_mimetype,
|
|
4167
4173
|
"file_filename": os.path.basename(file_path),
|
|
4168
|
-
"file": file_path
|
|
4174
|
+
"file": file_path, # icon_file
|
|
4169
4175
|
}
|
|
4170
4176
|
|
|
4171
4177
|
request_url = (
|