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/xml.py ADDED
@@ -0,0 +1,436 @@
1
+ """ XML helper module
2
+
3
+ Class: XML
4
+ Methods:
5
+
6
+ searchSetting: search a JSON-like setting inside an XML text telement
7
+ replaceInXmlFiles: Replace all occurrences of the search pattern with the replace string in all
8
+ XML files in the directory and its subdirectories.
9
+
10
+ """
11
+
12
+ __author__ = "Dr. Marc Diefenbruch"
13
+ __copyright__ = "Copyright 2023, OpenText"
14
+ __credits__ = ["Kai-Philip Gatzweiler"]
15
+ __maintainer__ = "Dr. Marc Diefenbruch"
16
+ __email__ = "mdiefenb@opentext.com"
17
+
18
+ import logging
19
+ import os
20
+ import re
21
+ # import regex as re
22
+
23
+ # we need lxml instead of stadard xml.etree to have xpath capabilities!
24
+ from lxml import etree
25
+
26
+ # import xml.etree.ElementTree as etree
27
+ from pyxecm.assoc import *
28
+
29
+ logger = logging.getLogger(os.path.basename(__file__))
30
+
31
+
32
+ class XML:
33
+ @classmethod
34
+ def getXmlElement(cls, xml_content: str, xpath: str):
35
+ # Parse XML content into an etree
36
+ tree = etree.fromstring(xml_content)
37
+
38
+ # Find the XML element specified by XPath
39
+ element = tree.find(xpath)
40
+
41
+ return element
42
+
43
+ @classmethod
44
+ def modifyXmlElement(cls, xml_content: str, xpath: str, new_value: str):
45
+ """Update the text (= content) of an XML element
46
+
47
+ Args:
48
+ xml_content (str): the content of an XML file
49
+ xpath (str): XML Path to identify the XML element
50
+ new_value (str): new text (content)
51
+ """
52
+ element = cls.getXmlElement(xml_content=xml_content, xpath=xpath)
53
+
54
+ if element is not None:
55
+ # Modify the XML element with the new value
56
+ element.text = new_value
57
+ else:
58
+ logger.warning("XML Element -> {} not found.".format(xpath))
59
+
60
+ @classmethod
61
+ def searchSetting(
62
+ cls,
63
+ element_text: str,
64
+ setting_key: str,
65
+ is_simple: bool = True,
66
+ is_escaped: bool = False,
67
+ ):
68
+ # the simple case covers settings like this:
69
+ # "syncCandidates":true,
70
+ # "syncCandidates":true,
71
+ # in this case the setting value is a scalar like true, false, a number or none
72
+ # the regular expression pattern searches for a setting name in "..." followed
73
+ # by a colon (:). The value is taken from what follows the colon until the next comma (,)
74
+ if is_simple:
75
+ if is_escaped:
76
+ pattern = r""{0}":[^,]*".format(setting_key)
77
+ else:
78
+ pattern = r'"{0}":[^,]*'.format(setting_key)
79
+ # the more complex case is a string value that may itself have commas,
80
+ # so we cannot look for comma as a delimiter like in the simple case
81
+ # but we take the value for a string delimited by double quotes ("...")
82
+ else:
83
+ if is_escaped:
84
+ pattern = r""{0}":".*"".format(setting_key)
85
+ else:
86
+ pattern = r'"{0}":"([^"]*)"'.format(setting_key)
87
+
88
+ match = re.search(pattern, element_text)
89
+ if match:
90
+ setting_line = match.group(0)
91
+ setting_value = setting_line.split(":")[1]
92
+ return setting_value
93
+ else:
94
+ return None
95
+
96
+ @classmethod
97
+ def replaceSetting(
98
+ cls,
99
+ element_text,
100
+ setting_key,
101
+ new_value: str,
102
+ is_simple: bool = True,
103
+ is_escaped: bool = False,
104
+ ):
105
+ if is_simple:
106
+ if is_escaped:
107
+ pattern = r""{0}":[^,]*".format(setting_key)
108
+ else:
109
+ pattern = r'"{0}":[^,]*'.format(setting_key)
110
+ else:
111
+ if is_escaped:
112
+ pattern = r""{0}":".*"".format(setting_key)
113
+ else:
114
+ pattern = r'"{0}":"([^"]*)"'.format(setting_key)
115
+
116
+ new_text = re.sub(pattern, new_value, element_text)
117
+
118
+ return new_text
119
+
120
+ @classmethod
121
+ def replaceInXmlFiles(
122
+ cls,
123
+ directory: str,
124
+ search_pattern: str,
125
+ replace_string: str,
126
+ xpath: str = "",
127
+ setting: str = "",
128
+ assoc_elem: str = "",
129
+ ) -> bool:
130
+ """Replaces all occurrences of the search pattern with the replace string in all XML files
131
+ in the directory and its subdirectories.
132
+
133
+ Args:
134
+ directory (string): directory to traverse for XML files
135
+ search_pattern (sting): string to search in the XML file. This can be empty
136
+ if xpath is used!
137
+ replace_string (string): replacement string
138
+ xpath (string): narrow down the replacement to an XML element that es defined by the XPath
139
+ for now the XPath needs to be constructed in a way the it returns
140
+ one or none element.
141
+ setting (string): narrow down the replacement to the line that includes the setting with this name.
142
+ This parameter is optional.
143
+ assoc_elem (string): lookup a specific assoc element. This parameter is optional.
144
+ Returns:
145
+ boolean: True if a replacement happened, False otherwise
146
+ """
147
+ # Define the regular expression pattern to search for
148
+ # search pattern can be empty if an xpath is used. So
149
+ # be careful here:
150
+ if search_pattern:
151
+ pattern = re.compile(search_pattern)
152
+
153
+ found = False
154
+
155
+ # Traverse the directory and its subdirectories
156
+ for subdir, dirs, files in os.walk(directory):
157
+ for file in files:
158
+ # Check if the file is an XML file
159
+ if file.endswith(".xml"):
160
+ # Read the contents of the file
161
+ file_path = os.path.join(subdir, file)
162
+
163
+ # if xpath is given we do an intelligent replacement
164
+ if xpath:
165
+ xml_modified = False
166
+ logger.info("Replacement with xpath...")
167
+ logger.info(
168
+ "XML path -> {}, setting -> {}, assoc element -> {}".format(
169
+ xpath, setting, assoc_elem
170
+ )
171
+ )
172
+ tree = etree.parse(file_path)
173
+ if not tree:
174
+ logger.erro(
175
+ "Cannot parse XML tree -> {}. Skipping...".format(
176
+ file_path
177
+ )
178
+ )
179
+ continue
180
+ root = tree.getroot()
181
+ # find the matching XML element using the given XPath:
182
+ elements = root.xpath(xpath)
183
+ if not elements:
184
+ logger.info(
185
+ "The XML file -> {} does not have any element with the given XML path -> {}. Skipping...".format(
186
+ file_path, xpath
187
+ )
188
+ )
189
+ continue
190
+ for element in elements:
191
+ # as XPath returns a list
192
+ # element = elements[0]
193
+ logger.info(
194
+ "Found XML element -> {} in -> {}".format(
195
+ element.tag, xpath
196
+ )
197
+ )
198
+ # the simple case: replace the complete text of the XML element
199
+ if not setting and not assoc_elem:
200
+ logger.info(
201
+ "Replace complete text of XML element -> {} from -> {} to -> {}".format(
202
+ xpath, element.text, replace_string
203
+ )
204
+ )
205
+ element.text = replace_string
206
+ xml_modified = True
207
+ # In this case we want to set a complete value of a setting (basically replacing a whole line)
208
+ elif setting and not assoc_elem:
209
+ logger.info(
210
+ "Replace single setting -> {} in XML element -> {} with new value -> {}".format(
211
+ setting, xpath, replace_string
212
+ )
213
+ )
214
+ setting_value = cls.searchSetting(
215
+ element.text, setting, is_simple=True
216
+ )
217
+ if setting_value:
218
+ logger.info(
219
+ "Found existing setting value -> {}".format(
220
+ setting_value
221
+ )
222
+ )
223
+ # replace_string = """ + setting + "":" + replace_string + ","
224
+ if (
225
+ replace_string == "true"
226
+ or replace_string == "false"
227
+ or replace_string == "none"
228
+ or replace_string.isnumeric()
229
+ ):
230
+ replace_setting = (
231
+ '"' + setting + '":' + replace_string
232
+ )
233
+ else:
234
+ replace_setting = (
235
+ '"' + setting + '":"' + replace_string + '"'
236
+ )
237
+ logger.info(
238
+ "Replacement setting -> {}".format(
239
+ replace_setting
240
+ )
241
+ )
242
+ element.text = cls.replaceSetting(
243
+ element_text=element.text,
244
+ setting_key=setting,
245
+ new_value=replace_setting,
246
+ is_simple=True,
247
+ )
248
+ xml_modified = True
249
+ else:
250
+ logger.warning(
251
+ "Cannot find the value for setting -> {}. Skipping...".format(
252
+ setting
253
+ )
254
+ )
255
+ continue
256
+ # in this case the text is just one assoc (no setting substructure)
257
+ elif not setting and assoc_elem:
258
+ logger.info(
259
+ "Replace single Assoc value -> {} in XML element -> {} with -> {}".format(
260
+ assoc_elem, xpath, replace_string
261
+ )
262
+ )
263
+ assoc_string: str = Assoc.extractAssocString(
264
+ input_string=element.text
265
+ )
266
+ logger.debug("Assoc String -> {}".format(assoc_string))
267
+ assoc_dict = Assoc.stringToDict(
268
+ assoc_string=assoc_string
269
+ )
270
+ logger.debug("Assoc Dict -> {}".format(assoc_dict))
271
+ assoc_dict[
272
+ assoc_elem
273
+ ] = replace_string # escaped_replace_string
274
+ assoc_string_new: str = Assoc.dictToString(
275
+ assoc_dict=assoc_dict
276
+ )
277
+ logger.info(
278
+ "Replace assoc with -> {}".format(assoc_string_new)
279
+ )
280
+ element.text = assoc_string_new
281
+ element.text = element.text.replace('"', """)
282
+ xml_modified = True
283
+ # In this case we have multiple settings with their own assocs
284
+ elif setting and assoc_elem:
285
+ logger.info(
286
+ "Replace single Assoc value -> {} in setting -> {} in XML element -> {} with -> {}".format(
287
+ assoc_elem, setting, xpath, replace_string
288
+ )
289
+ )
290
+ setting_value = cls.searchSetting(
291
+ element.text, setting, is_simple=False
292
+ )
293
+ if setting_value:
294
+ logger.info(
295
+ "Found setting value -> {}".format(
296
+ setting_value
297
+ )
298
+ )
299
+ assoc_string: str = Assoc.extractAssocString(
300
+ input_string=setting_value
301
+ )
302
+ logger.debug(
303
+ "Assoc String -> {}".format(assoc_string)
304
+ )
305
+ assoc_dict = Assoc.stringToDict(
306
+ assoc_string=assoc_string
307
+ )
308
+ logger.debug("Assoc Dict -> {}".format(assoc_dict))
309
+ escaped_replace_string = replace_string.replace(
310
+ "'", "\\\\\u0027"
311
+ )
312
+ logger.info(
313
+ "Escaped replacement string -> {}".format(
314
+ escaped_replace_string
315
+ )
316
+ )
317
+ assoc_dict[
318
+ assoc_elem
319
+ ] = escaped_replace_string # escaped_replace_string
320
+ assoc_string_new: str = Assoc.dictToString(
321
+ assoc_dict=assoc_dict
322
+ )
323
+ assoc_string_new = assoc_string_new.replace(
324
+ "'", "\\u0027"
325
+ )
326
+ # replace_setting = """ + setting + "":"" + assoc_string_new + """
327
+ replace_setting = (
328
+ '"' + setting + '":"' + assoc_string_new + '"'
329
+ )
330
+ logger.info(
331
+ "Replacement setting -> {}".format(
332
+ replace_setting
333
+ )
334
+ )
335
+ # here we need to apply a "trick". It is required
336
+ # as regexp cannot handle the special unicode escapes \u0027
337
+ # we require. We first insert a placeholder "PLACEHOLDER"
338
+ # and let regexp find the right place for it. Then further
339
+ # down we use a simple search&replace to switch the PLACEHOLDER
340
+ # to the real value (replace() does not have the issues with unicode escapes)
341
+ element.text = cls.replaceSetting(
342
+ element_text=element.text,
343
+ setting_key=setting,
344
+ # new_value=replace_setting,
345
+ new_value="PLACEHOLDER",
346
+ is_simple=False,
347
+ is_escaped=False,
348
+ )
349
+ element.text = element.text.replace(
350
+ "PLACEHOLDER", replace_setting
351
+ )
352
+ element.text = element.text.replace('"', """)
353
+ xml_modified = True
354
+ else:
355
+ logger.warning(
356
+ "Cannot find the value for setting -> {}. Skipping...".format(
357
+ setting
358
+ )
359
+ )
360
+ continue
361
+ if xml_modified:
362
+ logger.info(
363
+ "XML tree has been modified. Write updated file -> {}...".format(
364
+ file_path
365
+ )
366
+ )
367
+
368
+ new_contents = etree.tostring(
369
+ tree,
370
+ pretty_print=True,
371
+ xml_declaration=True,
372
+ encoding="UTF-8",
373
+ )
374
+ # we need to undo some of the stupid things tostring() did:
375
+ new_contents = new_contents.replace(
376
+ b""", b"""
377
+ )
378
+ new_contents = new_contents.replace(
379
+ b"'", b"'"
380
+ )
381
+ new_contents = new_contents.replace(b">", b">")
382
+ new_contents = new_contents.replace(b"<", b"<")
383
+
384
+ # Replace single quotes inside double quotes strings with "'" (manual escaping)
385
+ # This is required as we next want to replace all double quotes with single quotes
386
+ pattern = b'"([^"]*)"'
387
+ new_contents = re.sub(
388
+ pattern,
389
+ lambda m: m.group(0).replace(b"'", b"'"),
390
+ new_contents,
391
+ )
392
+
393
+ # Replace single quotes in XML text elements with "'"
394
+ # and replace double quotes in XML text elements with """
395
+ # This is required as we next want to replace all double quotes with single quotes
396
+ pattern = b'>([^<>]+?)<'
397
+ replacement = lambda match: match.group(0).replace(b'"', b"&quot;")
398
+ new_contents = re.sub(pattern, replacement, new_contents)
399
+ replacement = lambda match: match.group(0).replace(b"'", b"&apos;")
400
+ new_contents = re.sub(pattern, replacement, new_contents)
401
+
402
+ # Change double quotes to single quotes across the XML file - Extended ECM has it that way:
403
+ new_contents = new_contents.replace(b'"', b"'")
404
+
405
+ # Write the updated contents to the file.
406
+ # We DO NOT want to use tree.write() here
407
+ # as it would undo the manual XML tweaks we
408
+ # need for Extended ECM. We also need "wb"
409
+ # as we have bytes and not str as a data type
410
+ with open(file_path, "wb") as f:
411
+ f.write(new_contents)
412
+
413
+ found = True
414
+ # this is not using xpath - do a simple search and replace
415
+ else:
416
+ logger.info("Replacement without xpath...")
417
+ with open(file_path, "r") as f:
418
+ contents = f.read()
419
+ # Replace all occurrences of the search pattern with the replace string
420
+ new_contents = pattern.sub(replace_string, contents)
421
+
422
+ # Write the updated contents to the file if there were replacements
423
+ if contents != new_contents:
424
+ logger.info(
425
+ "Found search string -> {} in XML file -> {}. Write updated file...".format(
426
+ search_pattern, file_path
427
+ )
428
+ )
429
+ # Write the updated contents to the file
430
+ with open(file_path, "w") as f:
431
+ f.write(new_contents)
432
+ found = True
433
+
434
+ return found
435
+
436
+ # end method definition
@@ -1,11 +1,10 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyxecm
3
- Version: 0.0.18
3
+ Version: 0.0.19
4
4
  Summary: A Python library to interact with Opentext Extended ECM REST API
5
- Home-page: https://pypi.org/project/pyxecm/
6
5
  Author-email: Kai Gatzweiler <kgatzweiler@opentext.com>, "Dr. Marc Diefenbruch" <mdiefenb@opentext.com>
7
- Project-URL: Homepage, https://github.com/opentext/pyxecm
8
- Keywords: opentext extendedecm contentserver otds appworks archivecenter
6
+ Project-URL: Homepage, https://gitlab.otxlab.net/ecm/pyxecm
7
+ Keywords: opentext,extendedecm,contentserver,otds,appworks,archivecenter
9
8
  Classifier: Development Status :: 4 - Beta
10
9
  Classifier: Programming Language :: Python :: 3
11
10
  Classifier: License :: OSI Approved :: Apache Software License
@@ -25,7 +24,7 @@ Requires-Dist: suds
25
24
 
26
25
  # PYXECM
27
26
 
28
- A python library to interact with Opentext Extended ECM REST API.
27
+ A python library to interact with Opentext Extended ECM REST API.
29
28
  API documentation is available on [OpenText Developer](https://developer.opentext.com/ce/products/extendedecm)
30
29
 
31
30
 
@@ -0,0 +1,20 @@
1
+ pyxecm/__init__.py,sha256=aTminqJ8W9rcA4kIQUDSX3d7EE1f_8DhfbWwY1g6ZVw,448
2
+ pyxecm/assoc.py,sha256=G5QZVkAwIX-MDc7zog-otna60SyMhz-Gf1oZ9XhxNBk,4518
3
+ pyxecm/k8s.py,sha256=kNGc1kVFYKF9o3dRDdxQ4FMwC4b3QGgFX3SCnMv6k5k,33694
4
+ pyxecm/m365.py,sha256=9IguLxs3TqqbEFYABrYAEVAbjnwN8PLXHie6SdkmGtk,77794
5
+ pyxecm/main.py,sha256=hn-Xd6azipKptpw4ivvRwaQhot6V5AoQZLPgFP3J8oQ,51479
6
+ pyxecm/otac.py,sha256=sgdqHQu9tBMm5pMGKe5wb1dgMbHfxPGKgn4t5BCv-7E,9554
7
+ pyxecm/otcs.py,sha256=u0AKoaSmng2_EIg-Dhs_JOVEiEl-V_UhkzK7RPyKubw,256466
8
+ pyxecm/otds.py,sha256=lV0qmVuQj6cLQ0kMv8g7GPWBTTAnp9imYALnqUNQkVM,129349
9
+ pyxecm/otiv.py,sha256=i3-z0tJttNkaq1VoOfEkKgcVDjvkixUZeLRkxITom2o,1627
10
+ pyxecm/otpd.py,sha256=Djxno-r3XMkz6hb9qSLMecvdSa9RVmu6LpJeteLCx7o,10240
11
+ pyxecm/payload.py,sha256=K0RFd2y_CvwwAWNsS8Hk4FNT3EBNKMOgA4oebpPDCXs,297179
12
+ pyxecm/sap.py,sha256=T93T9mfE5HAJSZxF_0Cwvb-y7sNeef7GPgZenucnBok,5986
13
+ pyxecm/translate.py,sha256=dEqQAg6ZWcorjgobNnW9p-IP9iOQ6ouklntr4qrLqsI,2718
14
+ pyxecm/web.py,sha256=4ITGEQ7vOjMrp79e13k3lFB__q-4k5gDxg4T7kw765A,2719
15
+ pyxecm/xml.py,sha256=RsA30-rmNlY3smku6Fnt8V9PX2g4Jw1Fas5nVH_Cixc,21850
16
+ pyxecm-0.0.19.dist-info/LICENSE,sha256=z5DWWd5cHmQYJnq4BDt1bmVQjuXY1Qsp6y0v5ETCw-s,11360
17
+ pyxecm-0.0.19.dist-info/METADATA,sha256=TKigQumI5OrgBzxMksvuKZZWTvFevKWbgBlGx9tFoi0,1825
18
+ pyxecm-0.0.19.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
19
+ pyxecm-0.0.19.dist-info/top_level.txt,sha256=TGak3_dYN67ugKFbmRxRG1leDyOt0T7dypjdX4Ij1WE,7
20
+ pyxecm-0.0.19.dist-info/RECORD,,