pyxecm 0.0.17__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 CHANGED
@@ -2,16 +2,16 @@ import logging
2
2
  import os
3
3
 
4
4
  # pyxecm packages
5
- from pyxecm.main import *
6
- from pyxecm.k8s import *
7
- from pyxecm.otac import *
8
- from pyxecm.otcs import *
9
- from pyxecm.otds import *
10
- from pyxecm.otiv import *
11
- from pyxecm.otpd import *
12
- from pyxecm.payload import *
13
- from pyxecm.translate import *
14
- from pyxecm.web import *
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(request_url, headers=request_header)
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["OTAWP_RESSOURCE_ID"] = str(awp_resource_id)
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["OTAWP_RESSOURCE_ID"] = ""
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
- otiv_object,
1262
- m365_object,
1263
- placeholder_values,
1264
- True if LOGLEVEL == "DEBUG" else False,
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 re
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, parent_id: int, name: str, show_error: bool = False
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("Cannot get latest version of document with ID -> {}".format(node_id))
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.warning("Failed to to deploy workbench -> {}".format(workbench_name))
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 replacement["placeholder"] == replacement["value"]:
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
- "Placeholder and replacement are identical -> {}. Skipping...".format(
3291
- replacement["value"]
3292
- )
3260
+ "Replacement for transport -> {} is disabled. Skipping...".format(zip_file_path)
3293
3261
  )
3294
3262
  continue
3295
- logger.info(
3296
- "Replace -> {} with -> {} in Transport package -> {}".format(
3297
- replacement["placeholder"], replacement["value"], zip_file_folder
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
- found = self.replaceInXmlFiles(
3301
- zip_file_folder, replacement["placeholder"], replacement["value"]
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
- "Found replacement string -> {} in Transport package -> {}".format(
3306
- replacement["placeholder"], zip_file_folder
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
- "Did not find replacement string -> {} in Transport package -> {}".format(
3313
- replacement["placeholder"], zip_file_folder
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 found in transport -> {}".format(
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 #icon_file
4174
+ "file": file_path, # icon_file
4169
4175
  }
4170
4176
 
4171
4177
  request_url = (