pyxecm 1.6__py3-none-any.whl → 2.0.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.

Files changed (56) hide show
  1. pyxecm/__init__.py +6 -4
  2. pyxecm/avts.py +673 -246
  3. pyxecm/coreshare.py +686 -467
  4. pyxecm/customizer/__init__.py +16 -4
  5. pyxecm/customizer/__main__.py +58 -0
  6. pyxecm/customizer/api/__init__.py +5 -0
  7. pyxecm/customizer/api/__main__.py +6 -0
  8. pyxecm/customizer/api/app.py +914 -0
  9. pyxecm/customizer/api/auth.py +154 -0
  10. pyxecm/customizer/api/metrics.py +92 -0
  11. pyxecm/customizer/api/models.py +13 -0
  12. pyxecm/customizer/api/payload_list.py +865 -0
  13. pyxecm/customizer/api/settings.py +103 -0
  14. pyxecm/customizer/browser_automation.py +332 -139
  15. pyxecm/customizer/customizer.py +1007 -1130
  16. pyxecm/customizer/exceptions.py +35 -0
  17. pyxecm/customizer/guidewire.py +322 -0
  18. pyxecm/customizer/k8s.py +713 -378
  19. pyxecm/customizer/log.py +107 -0
  20. pyxecm/customizer/m365.py +2867 -909
  21. pyxecm/customizer/nhc.py +1169 -0
  22. pyxecm/customizer/openapi.py +258 -0
  23. pyxecm/customizer/payload.py +16817 -7467
  24. pyxecm/customizer/pht.py +699 -285
  25. pyxecm/customizer/salesforce.py +516 -342
  26. pyxecm/customizer/sap.py +58 -41
  27. pyxecm/customizer/servicenow.py +593 -371
  28. pyxecm/customizer/settings.py +442 -0
  29. pyxecm/customizer/successfactors.py +408 -346
  30. pyxecm/customizer/translate.py +83 -48
  31. pyxecm/helper/__init__.py +5 -2
  32. pyxecm/helper/assoc.py +83 -43
  33. pyxecm/helper/data.py +2406 -870
  34. pyxecm/helper/logadapter.py +27 -0
  35. pyxecm/helper/web.py +229 -101
  36. pyxecm/helper/xml.py +527 -171
  37. pyxecm/maintenance_page/__init__.py +5 -0
  38. pyxecm/maintenance_page/__main__.py +6 -0
  39. pyxecm/maintenance_page/app.py +51 -0
  40. pyxecm/maintenance_page/settings.py +28 -0
  41. pyxecm/maintenance_page/static/favicon.avif +0 -0
  42. pyxecm/maintenance_page/templates/maintenance.html +165 -0
  43. pyxecm/otac.py +234 -140
  44. pyxecm/otawp.py +1436 -557
  45. pyxecm/otcs.py +7716 -3161
  46. pyxecm/otds.py +2150 -919
  47. pyxecm/otiv.py +36 -21
  48. pyxecm/otmm.py +1272 -325
  49. pyxecm/otpd.py +231 -127
  50. pyxecm-2.0.0.dist-info/METADATA +145 -0
  51. pyxecm-2.0.0.dist-info/RECORD +54 -0
  52. {pyxecm-1.6.dist-info → pyxecm-2.0.0.dist-info}/WHEEL +1 -1
  53. pyxecm-1.6.dist-info/METADATA +0 -53
  54. pyxecm-1.6.dist-info/RECORD +0 -32
  55. {pyxecm-1.6.dist-info → pyxecm-2.0.0.dist-info/licenses}/LICENSE +0 -0
  56. {pyxecm-1.6.dist-info → pyxecm-2.0.0.dist-info}/top_level.txt +0 -0
pyxecm/helper/xml.py CHANGED
@@ -1,68 +1,82 @@
1
- """ XML helper module
2
-
3
- Class: XML
4
- Methods:
5
-
6
- load_xml_file: Load an XML file into a Python list of dictionaries
7
- load_xml_files_from_directory: Load all XML files from a directory that matches defined file names
8
- then using the XPath to identify a set of elements and convert them
9
- into a Python list of dictionaries.
10
- get_xml_element: Retrieve an XML Element from a string using an XPath expression
11
- modify_xml_element: Update the text (= content) of an XML element
12
- search_setting: Search a JSON-like setting inside an XML text telement
13
- replace_setting: Update a setting value
14
- replace_in_xml_files: Replace all occurrences of the search pattern with the replace string in all
15
- XML files in the directory and its subdirectories.
16
-
17
- """
1
+ """XML helper module."""
18
2
 
19
3
  __author__ = "Dr. Marc Diefenbruch"
20
- __copyright__ = "Copyright 2024, OpenText"
4
+ __copyright__ = "Copyright (C) 2024-2025, OpenText"
21
5
  __credits__ = ["Kai-Philip Gatzweiler"]
22
6
  __maintainer__ = "Dr. Marc Diefenbruch"
23
7
  __email__ = "mdiefenb@opentext.com"
24
8
 
9
+ import fnmatch
10
+ import glob
25
11
  import logging
26
12
  import os
27
13
  import re
28
- import fnmatch
29
14
  import zipfile
15
+ from concurrent.futures import ThreadPoolExecutor
16
+ from queue import Queue
17
+
18
+ import xmltodict
30
19
 
31
20
  # we need lxml instead of stadard xml.etree to have xpath capabilities!
32
21
  from lxml import etree
33
- import xmltodict
22
+ from lxml.etree import Element
34
23
 
35
- # import xml.etree.ElementTree as etree
36
- from pyxecm.helper.assoc import Assoc
24
+ from pyxecm.helper import Assoc
37
25
 
38
- logger = logging.getLogger("pyxecm.xml")
26
+ default_logger = logging.getLogger("pyxecm.helper.xml")
39
27
 
40
28
 
41
29
  class XML:
42
- """XML Class to handle XML processing, e.g. to parse and update Extended ECM transport packages"""
30
+ """Handle XML processing, e.g. to parse and update Extended ECM transport packages."""
31
+
32
+ logger: logging.Logger = default_logger
43
33
 
44
34
  @classmethod
45
35
  def load_xml_file(
46
- cls, file_path: str, xpath: str, dir_name: str | None = None
36
+ cls,
37
+ file_path: str,
38
+ xpath: str,
39
+ dir_name: str | None = None,
40
+ logger: logging.Logger = default_logger,
47
41
  ) -> list | None:
48
- """Load an XML file into a Python list of dictionaries
42
+ """Load an XML file into a Python list of dictionaries.
49
43
 
50
44
  Args:
51
- file_path (str): Path to XML file
52
- xpath (str): XPath to select sub-elements
45
+ file_path (str):
46
+ The path to XML file.
47
+ xpath (str):
48
+ XPath to select sub-elements.
49
+ dir_name (str | None, optional):
50
+ Directory name to include in each dictionary, if provided.
51
+ logger (logging.Logger):
52
+ The logging object used for all log messages.
53
53
 
54
54
  Returns:
55
- dict | None: _description_
55
+ dict | None:
56
+ A list of dictionaries representing the parsed XML elements,
57
+ or None if an error occurs during file reading or parsing.
58
+
56
59
  """
57
60
 
58
- try:
61
+ if not os.path.exists(file_path):
62
+ logger.error("XML File -> %s does not exist!", file_path)
63
+ return None
59
64
 
65
+ try:
60
66
  tree = etree.parse(file_path)
61
67
  if not tree:
62
- return []
63
-
64
- # Perform the XPath query to select 'child' elements
65
- elements = tree.xpath(xpath) # Adjust XPath as needed
68
+ logger.warning("Empty or invalid XML tree for file -> %s", file_path)
69
+ return None
70
+
71
+ # Extract elements using the XPath:
72
+ elements = tree.xpath(xpath)
73
+ if not elements:
74
+ logger.warning(
75
+ "No elements matched XPath -> %s in file -> '%s'",
76
+ xpath,
77
+ file_path,
78
+ )
79
+ return None
66
80
 
67
81
  # Convert the selected elements to dictionaries
68
82
  results = []
@@ -75,12 +89,15 @@ class XML:
75
89
  element_dict["directory"] = dir_name
76
90
  results.append(element_dict)
77
91
 
78
- except IOError as e:
79
- logger.error("IO Error -> %s", str(e))
80
- except etree.XMLSyntaxError as e:
81
- logger.error("XML Syntax Error -> %s", str(e))
82
- except etree.DocumentInvalid as e:
83
- logger.error("Document Invalid -> %s", str(e))
92
+ except OSError:
93
+ logger.error("IO Error with file -> %s", file_path)
94
+ return None
95
+ except etree.XMLSyntaxError:
96
+ logger.error("XML Syntax Error in file -> %s", file_path)
97
+ return None
98
+ except etree.DocumentInvalid:
99
+ logger.error("Invalid XML document -> %s", file_path)
100
+ return None
84
101
 
85
102
  return results
86
103
 
@@ -88,33 +105,50 @@ class XML:
88
105
 
89
106
  @classmethod
90
107
  def load_xml_files_from_directory(
91
- cls, path_to_root: str, filenames: list | None, xpath: str | None = None
108
+ cls,
109
+ path_to_root: str,
110
+ filenames: list | None,
111
+ xpath: str | None = None,
112
+ logger: logging.Logger = default_logger,
92
113
  ) -> list | None:
93
- """Load all XML files from a directory that matches defined file names
94
- then using the XPath to identify a set of elements and convert them
95
- into a Python list of dictionaries.
114
+ """Load all XML files from a directory that matches defined file names.
115
+
116
+ Then using the XPath to identify a set of elements and convert them
117
+ into a Python list of dictionaries.
96
118
 
97
119
  Args:
98
- path_to_root (str): Path to the root element of the
99
- directory structure
100
- filenames (list): list of filenames. If empty all filenames ending
101
- with ".xml" are used.
102
- xpath (str, optional): XPath to the elements we want to select
120
+ path_to_root (str):
121
+ Path to the root element of the
122
+ directory structure
123
+ filenames (list):
124
+ A list of filenames. This can also be patterns like
125
+ "*/en/docovw.xml". If empty all filenames ending
126
+ with ".xml" is used.
127
+ xpath (str, optional):
128
+ The XPath to the elements we want to select.
129
+ logger (logging.Logger):
130
+ The logging object used for all log messages.
103
131
 
104
132
  Returns:
105
- list: List of dictionaries
133
+ list:
134
+ List of dictionaries.
135
+
106
136
  """
107
137
 
108
- try:
138
+ if not filenames:
139
+ filenames = ["*.xml"]
109
140
 
110
- # Check if the provided path is a directory
141
+ try:
142
+ # Check if the provided path is a directory or a zip file that can be extracted
143
+ # into a directory:
111
144
  if not os.path.isdir(path_to_root) and not path_to_root.endswith(".zip"):
112
145
  logger.error(
113
- "The provided path '%s' is not a valid directory or Zip file.",
146
+ "The provided path -> '%s' is not a valid directory or Zip file.",
114
147
  path_to_root,
115
148
  )
116
- return False
149
+ return None
117
150
 
151
+ # If we have a zip file we extract it - but only if it has not been extracted before:
118
152
  if path_to_root.endswith(".zip"):
119
153
  zip_file_folder = os.path.splitext(path_to_root)[0]
120
154
  if not os.path.exists(zip_file_folder):
@@ -123,8 +157,21 @@ class XML:
123
157
  path_to_root,
124
158
  zip_file_folder,
125
159
  )
126
- with zipfile.ZipFile(path_to_root, "r") as zfile:
127
- zfile.extractall(zip_file_folder)
160
+ try:
161
+ with zipfile.ZipFile(path_to_root, "r") as zfile:
162
+ zfile.extractall(zip_file_folder)
163
+ except zipfile.BadZipFile:
164
+ logger.error(
165
+ "Failed to extract zip file -> '%s'",
166
+ path_to_root,
167
+ )
168
+ return None
169
+ except OSError:
170
+ logger.error(
171
+ "OS error occurred while trying to extract -> '%s'",
172
+ path_to_root,
173
+ )
174
+ return None
128
175
  else:
129
176
  logger.info(
130
177
  "Zip file is already extracted (path -> '%s' exists). Reusing extracted data...",
@@ -142,37 +189,308 @@ class XML:
142
189
  file_name = os.path.basename(file_path)
143
190
  dir_name = os.path.dirname(file_path)
144
191
 
145
- if any(
146
- fnmatch.fnmatch(file_path, pattern) for pattern in filenames
147
- ) and file_name.endswith(".xml"):
192
+ if any(fnmatch.fnmatch(file_path, pattern) for pattern in filenames) and file_name.endswith(".xml"):
148
193
  logger.info(
149
- "Load XML file -> '%s' of size -> %s", file_path, file_size
194
+ "Load XML file -> '%s' of size -> %s",
195
+ file_path,
196
+ file_size,
150
197
  )
151
- results += cls.load_xml_file(
152
- file_path, xpath=xpath, dir_name=dir_name
198
+ elements = cls.load_xml_file(
199
+ file_path,
200
+ xpath=xpath,
201
+ dir_name=dir_name,
153
202
  )
203
+ if elements:
204
+ results += elements
205
+
206
+ except NotADirectoryError:
207
+ logger.error(
208
+ "The given path -> '%s' is not a directory!",
209
+ path_to_root,
210
+ )
211
+ return None
212
+ except FileNotFoundError:
213
+ logger.error(
214
+ "The given path -> '%s' does not exist!",
215
+ path_to_root,
216
+ )
217
+ return None
218
+ except PermissionError:
219
+ logger.error(
220
+ "No permission to access path -> '%s'!",
221
+ path_to_root,
222
+ )
223
+ return None
224
+ except OSError:
225
+ logger.error("Low level OS error with file -> %s", path_to_root)
226
+ return None
227
+
228
+ return results
229
+
230
+ # end method definition
231
+
232
+ @classmethod
233
+ def load_xml_files_from_directories(
234
+ cls,
235
+ directories: list[str],
236
+ filenames: list[str] | None = None,
237
+ xpath: str | None = None,
238
+ logger: logging.Logger = default_logger,
239
+ ) -> list[dict] | None:
240
+ """Load XML files from multiple directories or zip files concurrently.
241
+
242
+ Process them using XPath, and return a list of dictionaries containing the extracted elements.
243
+
244
+ This method handles multiple directories or zip files, processes XML files inside them in parallel
245
+ using threads, and extracts elements that match the specified XPath. It also supports pattern matching
246
+ for filenames and handles errors such as missing files or permission issues.
247
+
248
+ Args:
249
+ directories (list[str]):
250
+ A list of directories or zip files to process. Each item can be a path
251
+ to a directory or a zip file that contains XML files.
252
+ filenames (list[str] | None, optional):
253
+ A list of filename patterns (e.g., ["*/en/docovw.xml"]) to match
254
+ against the XML files. If None or empty, defaults to ["*.xml"].
255
+ xpath (str | None, optional):
256
+ An optional XPath string used to filter elements from the XML files.
257
+ logger (logging.Logger):
258
+ The logging object used for all log messages.
259
+
260
+ Returns:
261
+ list[dict] | None:
262
+ A list of dictionaries containing the extracted XML elements. Returns None
263
+ if any error occurs during processing.
264
+
265
+ Raises:
266
+ Exception: If any error occurs during processing, such as issues with directories, files, or zip extraction.
267
+
268
+ """
269
+
270
+ # Set default for filenames if not provided
271
+ if not filenames:
272
+ filenames = ["*.xml"]
273
+
274
+ results_queue = Queue()
275
+
276
+ def process_xml_file(file_path: str) -> None:
277
+ """Process a single XML file.
278
+
279
+ Args:
280
+ file_path (str):
281
+ Path to the XML file.
282
+
283
+ Results:
284
+ Adds elements to the result_queue defined outside this sub-method.
285
+
286
+ """
287
+
288
+ try:
289
+ file_size = os.path.getsize(file_path)
290
+ file_name = os.path.basename(file_path)
291
+ dir_name = os.path.dirname(file_path)
292
+
293
+ if (
294
+ not filenames or any(fnmatch.fnmatch(file_path, pattern) for pattern in filenames)
295
+ ) and file_name.endswith(".xml"):
296
+ logger.info(
297
+ "Load XML file -> '%s' of size -> %s",
298
+ file_path,
299
+ file_size,
300
+ )
301
+ elements = cls.load_xml_file(
302
+ file_path,
303
+ xpath=xpath,
304
+ dir_name=dir_name,
305
+ )
306
+ if elements:
307
+ results_queue.put(elements)
308
+ except FileNotFoundError:
309
+ logger.error("File not found -> '%s'!", file_path)
310
+ except PermissionError:
311
+ logger.error(
312
+ "Permission error with file -> '%s'!",
313
+ file_path,
314
+ )
315
+ except OSError:
316
+ logger.error(
317
+ "OS error processing file -> '%s'!",
318
+ file_path,
319
+ )
320
+ except ValueError:
321
+ logger.error(
322
+ "Value error processing file -> '%s'!",
323
+ file_path,
324
+ )
325
+
326
+ # end method process_xml_file
154
327
 
155
- except NotADirectoryError as nde:
156
- logger.error("Error -> %s", str(nde))
157
- except FileNotFoundError as fnfe:
158
- logger.error("Error -> %s", str(fnfe))
159
- except PermissionError as pe:
160
- logger.error("Error -> %s", str(pe))
328
+ def process_directory_or_zip(path_to_root: str) -> list | None:
329
+ """Process all files in a directory or zip file.
330
+
331
+ Args:
332
+ path_to_root (str):
333
+ File path to the root directory or zip file.
334
+
335
+ """
336
+
337
+ try:
338
+ # Handle zip files
339
+ if path_to_root.endswith(".zip"):
340
+ zip_file_folder = os.path.splitext(path_to_root)[0]
341
+ if not os.path.exists(zip_file_folder):
342
+ logger.info(
343
+ "Unzipping -> '%s' into folder -> '%s'...",
344
+ path_to_root,
345
+ zip_file_folder,
346
+ )
347
+ try:
348
+ with zipfile.ZipFile(path_to_root, "r") as zfile:
349
+ zfile.extractall(zip_file_folder)
350
+ except zipfile.BadZipFile:
351
+ logger.error(
352
+ "Bad zip file -> '%s'!",
353
+ path_to_root,
354
+ )
355
+ except zipfile.LargeZipFile:
356
+ logger.error(
357
+ "Zip file is too large to process -> '%s'!",
358
+ path_to_root,
359
+ )
360
+ except PermissionError:
361
+ logger.error(
362
+ "Permission error extracting zip file -> '%s'!",
363
+ path_to_root,
364
+ )
365
+ except OSError:
366
+ logger.error(
367
+ "OS error occurred while extracting zip file -> '%s'!",
368
+ path_to_root,
369
+ )
370
+ return # Don't proceed further if zip extraction fails
371
+
372
+ else:
373
+ logger.info(
374
+ "Zip file is already extracted (path -> '%s' exists). Reusing extracted data...",
375
+ zip_file_folder,
376
+ )
377
+ path_to_root = zip_file_folder
378
+ # end if path_to_root.endswith(".zip")
379
+
380
+ # Use inner threading to process files within the directory
381
+ with ThreadPoolExecutor(
382
+ thread_name_prefix="ProcessXMLFile",
383
+ ) as inner_executor:
384
+ for root, _, files in os.walk(path_to_root):
385
+ for file_data in files:
386
+ file_path = os.path.join(root, file_data)
387
+ inner_executor.submit(process_xml_file, file_path)
388
+
389
+ except FileNotFoundError:
390
+ logger.error(
391
+ "Directory or file not found -> '%s'!",
392
+ path_to_root,
393
+ )
394
+ except PermissionError:
395
+ logger.error(
396
+ "Permission error with directory -> '%s'!",
397
+ path_to_root,
398
+ )
399
+ except OSError:
400
+ logger.error(
401
+ "OS error processing path -> '%s'!",
402
+ path_to_root,
403
+ )
404
+ except ValueError:
405
+ logger.error(
406
+ "Value error processing path -> '%s'!",
407
+ path_to_root,
408
+ )
409
+
410
+ # end method process_directory_or_zip
411
+
412
+ try:
413
+ # Resolve wildcards in the directories list
414
+ expanded_directories: list[str] = []
415
+ for directory in directories:
416
+ if "*" in directory:
417
+ expanded_directory: list = glob.glob(directory)
418
+ logger.info(
419
+ "Expanding directory -> '%s' with wildcards...",
420
+ directory,
421
+ )
422
+ expanded_directories.extend(expanded_directory)
423
+ else:
424
+ logger.info(
425
+ "Directory -> '%s' has no wildcards. Not expanding...",
426
+ directory,
427
+ )
428
+ expanded_directories.append(directory)
429
+
430
+ # Use ThreadPoolExecutor for outer level: processing directories/zip files
431
+ logger.info(
432
+ "Starting %d threads for each directory or zip file...",
433
+ len(expanded_directories),
434
+ )
435
+ with ThreadPoolExecutor(
436
+ thread_name_prefix="ProcessDirOrZip",
437
+ ) as outer_executor:
438
+ futures = [
439
+ outer_executor.submit(process_directory_or_zip, directory) for directory in expanded_directories
440
+ ]
441
+
442
+ # Wait for all futures to complete
443
+ for future in futures:
444
+ future.result()
445
+
446
+ # Collect results from the queue
447
+ logger.info("Collecting results from worker queue...")
448
+ results = []
449
+ while not results_queue.empty():
450
+ results.extend(results_queue.get())
451
+ logger.info("Done. Collected %d results.", len(results))
452
+
453
+ except FileNotFoundError:
454
+ logger.error(
455
+ "Directory or file not found during execution!",
456
+ )
457
+ return None
458
+ except PermissionError:
459
+ logger.error("Permission error during execution!")
460
+ return None
461
+ except TimeoutError:
462
+ logger.error(
463
+ "Timeout occurred while waiting for threads!",
464
+ )
465
+ return None
466
+ except BrokenPipeError:
467
+ logger.error(
468
+ "Broken pipe error occurred during thread communication!",
469
+ )
470
+ return None
161
471
 
162
472
  return results
163
473
 
164
474
  # end method definition
165
475
 
166
476
  @classmethod
167
- def get_xml_element(cls, xml_content: str, xpath: str):
168
- """Retrieves an XML Element from a string using an XPath expression
477
+ def get_xml_element(
478
+ cls,
479
+ xml_content: str,
480
+ xpath: str,
481
+ ) -> Element:
482
+ """Retrieve an XML Element from a string using an XPath expression.
169
483
 
170
484
  Args:
171
- xml_content (str): XML file as a string
172
- xpath (str): XPath to find the element
485
+ xml_content (str):
486
+ XML file as a string
487
+ xpath (str):
488
+ XPath used to find the element.
173
489
 
174
490
  Returns:
175
- str: text of element
491
+ Element:
492
+ The XML element.
493
+
176
494
  """
177
495
 
178
496
  # Parse XML content into an etree
@@ -186,13 +504,25 @@ class XML:
186
504
  # end method definition
187
505
 
188
506
  @classmethod
189
- def modify_xml_element(cls, xml_content: str, xpath: str, new_value: str):
190
- """Update the text (= content) of an XML element
507
+ def modify_xml_element(
508
+ cls,
509
+ xml_content: str,
510
+ xpath: str,
511
+ new_value: str,
512
+ logger: logging.Logger = default_logger,
513
+ ) -> None:
514
+ """Update the text (= content) of an XML element.
191
515
 
192
516
  Args:
193
- xml_content (str): the content of an XML file
194
- xpath (str): XML Path to identify the XML element
195
- new_value (str): new text (content)
517
+ xml_content (str):
518
+ The content of an XML file.
519
+ xpath (str):
520
+ XML Path to identify the XML element.
521
+ new_value (str):
522
+ The new text (content).
523
+ logger (logging.Logger):
524
+ The logging object used for all log messages.
525
+
196
526
  """
197
527
  element = cls.get_xml_element(xml_content=xml_content, xpath=xpath)
198
528
 
@@ -212,7 +542,7 @@ class XML:
212
542
  is_simple: bool = True,
213
543
  is_escaped: bool = False,
214
544
  ) -> str | None:
215
- """Search a setting in an XML element and return its value
545
+ """Search a setting in an XML element and return its value.
216
546
 
217
547
  The simple case covers settings like this:
218
548
  "syncCandidates":true,
@@ -226,25 +556,27 @@ class XML:
226
556
  but we take the value for a string delimited by double quotes ("...")
227
557
 
228
558
  Args:
229
- element_text (str): the text to examine - typically content of an XML element
230
- setting_key (str): name of the setting key (before the colon)
231
- is_simple (bool, optional): True if the value is scalar (not having assocs with commas). Defaults to True.
232
- is_escaped (bool, optional): True if the quotes or escaped with ". Defaults to False.
559
+ element_text (str):
560
+ The text to examine - typically content of an XML element.
561
+ setting_key (str):
562
+ The name of the setting key (before the colon).
563
+ is_simple (bool, optional):
564
+ True if the value is scalar (not having assocs with commas). Defaults to True.
565
+ is_escaped (bool, optional):
566
+ True if the quotes or escaped with ". Defaults to False.
233
567
 
234
568
  Returns:
235
- str: the value of the setting or None if the setting is not found.
569
+ str:
570
+ The value of the setting or None if the setting is not found.
571
+
236
572
  """
237
573
 
238
574
  if is_simple:
239
- if is_escaped:
240
- pattern = r""{0}":[^,]*".format(setting_key)
241
- else:
242
- pattern = r'"{0}":[^,]*'.format(setting_key)
575
+ pattern = r""{}":[^,]*".format(setting_key) if is_escaped else r'"{}":[^,]*'.format(setting_key)
576
+ elif is_escaped:
577
+ pattern = r""{}":".*"".format(setting_key)
243
578
  else:
244
- if is_escaped:
245
- pattern = r""{0}":".*"".format(setting_key)
246
- else:
247
- pattern = r'"{0}":"([^"]*)"'.format(setting_key)
579
+ pattern = r'"{}":"([^"]*)"'.format(setting_key)
248
580
 
249
581
  match = re.search(pattern, element_text)
250
582
  if match:
@@ -279,26 +611,29 @@ class XML:
279
611
  but we take the value for a string delimited by double quotes ("...")
280
612
 
281
613
  Args:
282
- element_text (str): original text of the XML element (that is to be updated)
283
- setting_key (str): name of the setting
284
- new_value (str): new value of the setting
285
- is_simple (bool, optional): True = value is a scalar like true, false, a number or none. Defaults to True.
286
- is_escaped (bool, optional): True if the value is surrrounded with ". Defaults to False.
614
+ element_text (str):
615
+ The original text of the XML element (that is to be updated).
616
+ setting_key (str):
617
+ The name of the setting.
618
+ new_value (str):
619
+ The new value of the setting.
620
+ is_simple (bool, optional):
621
+ True = value is a scalar like true, false, a number or none. Defaults to True.
622
+ is_escaped (bool, optional):
623
+ True if the value is surrrounded with ". Defaults to False.
287
624
 
288
625
  Returns:
289
- str: updated element text
626
+ str:
627
+ The updated element text.
628
+
290
629
  """
291
630
 
292
631
  if is_simple:
293
- if is_escaped:
294
- pattern = r""{0}":[^,]*".format(setting_key)
295
- else:
296
- pattern = r'"{0}":[^,]*'.format(setting_key)
632
+ pattern = r""{}":[^,]*".format(setting_key) if is_escaped else r'"{}":[^,]*'.format(setting_key)
633
+ elif is_escaped:
634
+ pattern = r""{}":".*"".format(setting_key)
297
635
  else:
298
- if is_escaped:
299
- pattern = r""{0}":".*"".format(setting_key)
300
- else:
301
- pattern = r'"{0}":"([^"]*)"'.format(setting_key)
636
+ pattern = r'"{}":"([^"]*)"'.format(setting_key)
302
637
 
303
638
  new_text = re.sub(pattern, new_value, element_text)
304
639
 
@@ -315,24 +650,38 @@ class XML:
315
650
  xpath: str = "",
316
651
  setting: str = "",
317
652
  assoc_elem: str = "",
653
+ logger: logging.Logger = default_logger,
318
654
  ) -> bool:
319
- """Replaces all occurrences of the search pattern with the replace string in all XML files
320
- in the directory and its subdirectories.
655
+ """Replace all occurrences of the search pattern with the replace string.
656
+
657
+ This is done in all XML files in the directory and its subdirectories.
321
658
 
322
659
  Args:
323
- directory (str): directory to traverse for XML files
324
- search_pattern (str): string to search in the XML file. This can be empty
325
- if xpath is used!
326
- replace_string (str): replacement string
327
- xpath (str): narrow down the replacement to an XML element that es defined by the XPath
328
- for now the XPath needs to be constructed in a way the it returns
329
- one or none element.
330
- setting (str): narrow down the replacement to the line that includes the setting with this name.
331
- This parameter is optional.
332
- assoc_elem (str): lookup a specific assoc element. This parameter is optional.
660
+ directory (str):
661
+ Directory to traverse for XML files
662
+ search_pattern (str):
663
+ The string to search in the XML file.
664
+ This can be empty if xpath is used!
665
+ replace_string (str):
666
+ The replacement string.
667
+ xpath (str, optional):
668
+ An XPath can be given to narrow down the replacement to an XML element.
669
+ For now the XPath needs to be constructed in a way the it returns
670
+ one or none element.
671
+ setting (str, optional):
672
+ Narrow down the replacement to the line that includes the setting with this name.
673
+ This parameter is optional.
674
+ assoc_elem (str, optional):
675
+ Lookup a specific assoc element. This parameter is optional.
676
+ logger (logging.Logger):
677
+ The logging object used for all log messages.
678
+
333
679
  Returns:
334
- bool: True if a replacement happened, False otherwise
680
+ bool:
681
+ True if a replacement happened, False otherwise
682
+
335
683
  """
684
+
336
685
  # Define the regular expression pattern to search for
337
686
  # search pattern can be empty if an xpath is used. So
338
687
  # be careful here:
@@ -362,7 +711,8 @@ class XML:
362
711
  tree = etree.parse(file_path)
363
712
  if not tree:
364
713
  logger.error(
365
- "Cannot parse XML tree -> %s. Skipping...", file_path
714
+ "Cannot parse XML tree -> %s. Skipping...",
715
+ file_path,
366
716
  )
367
717
  continue
368
718
  root = tree.getroot()
@@ -402,7 +752,9 @@ class XML:
402
752
  replace_string,
403
753
  )
404
754
  setting_value = cls.search_setting(
405
- element.text, setting, is_simple=True
755
+ element.text,
756
+ setting,
757
+ is_simple=True,
406
758
  )
407
759
  if setting_value:
408
760
  logger.debug(
@@ -411,21 +763,13 @@ class XML:
411
763
  )
412
764
  # Check if the setting value needs to be surrounded by quotes.
413
765
  # Only simplistic values like booleans or numeric values don't need quotes
414
- if (
415
- replace_string == "true"
416
- or replace_string == "false"
417
- or replace_string == "none"
418
- or replace_string.isnumeric()
419
- ):
420
- replace_setting = (
421
- '"' + setting + '":' + replace_string
422
- )
766
+ if replace_string in ("true", "false", "none") or replace_string.isnumeric():
767
+ replace_setting = '"' + setting + '":' + replace_string
423
768
  else:
424
- replace_setting = (
425
- '"' + setting + '":"' + replace_string + '"'
426
- )
769
+ replace_setting = '"' + setting + '":"' + replace_string + '"'
427
770
  logger.debug(
428
- "Replacement setting -> %s", replace_setting
771
+ "Replacement setting -> %s",
772
+ replace_setting,
429
773
  )
430
774
  element.text = cls.replace_setting(
431
775
  element_text=element.text,
@@ -449,21 +793,20 @@ class XML:
449
793
  replace_string,
450
794
  )
451
795
  assoc_string: str = Assoc.extract_assoc_string(
452
- input_string=element.text
796
+ input_string=element.text,
453
797
  )
454
798
  logger.debug("Assoc String -> %s", assoc_string)
455
799
  assoc_dict = Assoc.string_to_dict(
456
- assoc_string=assoc_string
800
+ assoc_string=assoc_string,
457
801
  )
458
802
  logger.debug("Assoc Dict -> %s", str(assoc_dict))
459
- assoc_dict[assoc_elem] = (
460
- replace_string # escaped_replace_string
461
- )
803
+ assoc_dict[assoc_elem] = replace_string # escaped_replace_string
462
804
  assoc_string_new: str = Assoc.dict_to_string(
463
- assoc_dict=assoc_dict
805
+ assoc_dict=assoc_dict,
464
806
  )
465
807
  logger.debug(
466
- "Replace assoc with -> %s", assoc_string_new
808
+ "Replace assoc with -> %s",
809
+ assoc_string_new,
467
810
  )
468
811
  element.text = assoc_string_new
469
812
  element.text = element.text.replace('"', """)
@@ -478,42 +821,43 @@ class XML:
478
821
  replace_string,
479
822
  )
480
823
  setting_value = cls.search_setting(
481
- element.text, setting, is_simple=False
824
+ element.text,
825
+ setting,
826
+ is_simple=False,
482
827
  )
483
828
  if setting_value:
484
829
  logger.debug(
485
- "Found setting value -> %s", setting_value
830
+ "Found setting value -> %s",
831
+ setting_value,
486
832
  )
487
833
  assoc_string: str = Assoc.extract_assoc_string(
488
- input_string=setting_value
834
+ input_string=setting_value,
489
835
  )
490
836
  logger.debug("Assoc String -> %s", assoc_string)
491
837
  assoc_dict = Assoc.string_to_dict(
492
- assoc_string=assoc_string
838
+ assoc_string=assoc_string,
493
839
  )
494
840
  logger.debug("Assoc Dict -> %s", str(assoc_dict))
495
841
  escaped_replace_string = replace_string.replace(
496
- "'", "\\\\\u0027"
842
+ "'",
843
+ "\\\\\u0027",
497
844
  )
498
845
  logger.debug(
499
846
  "Escaped replacement string -> %s",
500
847
  escaped_replace_string,
501
848
  )
502
- assoc_dict[assoc_elem] = (
503
- escaped_replace_string # escaped_replace_string
504
- )
849
+ assoc_dict[assoc_elem] = escaped_replace_string # escaped_replace_string
505
850
  assoc_string_new: str = Assoc.dict_to_string(
506
- assoc_dict=assoc_dict
851
+ assoc_dict=assoc_dict,
507
852
  )
508
853
  assoc_string_new = assoc_string_new.replace(
509
- "'", "\\u0027"
510
- )
511
- # replace_setting = """ + setting + "":"" + assoc_string_new + """
512
- replace_setting = (
513
- '"' + setting + '":"' + assoc_string_new + '"'
854
+ "'",
855
+ "\\u0027",
514
856
  )
857
+ replace_setting = '"' + setting + '":"' + assoc_string_new + '"'
515
858
  logger.debug(
516
- "Replacement setting -> %s", replace_setting
859
+ "Replacement setting -> %s",
860
+ replace_setting,
517
861
  )
518
862
  # here we need to apply a "trick". It is required
519
863
  # as regexp cannot handle the special unicode escapes \u0027
@@ -524,13 +868,13 @@ class XML:
524
868
  element.text = cls.replace_setting(
525
869
  element_text=element.text,
526
870
  setting_key=setting,
527
- # new_value=replace_setting,
528
871
  new_value="PLACEHOLDER",
529
872
  is_simple=False,
530
873
  is_escaped=False,
531
874
  )
532
875
  element.text = element.text.replace(
533
- "PLACEHOLDER", replace_setting
876
+ "PLACEHOLDER",
877
+ replace_setting,
534
878
  )
535
879
  element.text = element.text.replace('"', """)
536
880
  xml_modified = True
@@ -554,10 +898,12 @@ class XML:
554
898
  )
555
899
  # we need to undo some of the stupid things tostring() did:
556
900
  new_contents = new_contents.replace(
557
- b""", b"""
901
+ b""",
902
+ b""",
558
903
  )
559
904
  new_contents = new_contents.replace(
560
- b"'", b"'"
905
+ b"'",
906
+ b"'",
561
907
  )
562
908
  new_contents = new_contents.replace(b">", b">")
563
909
  new_contents = new_contents.replace(b"<", b"<")
@@ -576,12 +922,14 @@ class XML:
576
922
  # This is required as we next want to replace all double quotes with single quotes
577
923
  # to make the XML files as similar as possible with Extended ECM's format
578
924
  pattern = b">([^<>]+?)<"
579
- replacement = lambda match: match.group(0).replace(
580
- b'"', b"&quot;"
925
+ replacement = lambda match: match.group(0).replace( # noqa: E731
926
+ b'"',
927
+ b"&quot;",
581
928
  )
582
929
  new_contents = re.sub(pattern, replacement, new_contents)
583
- replacement = lambda match: match.group(0).replace(
584
- b"'", b"&apos;"
930
+ replacement = lambda match: match.group(0).replace( # noqa: E731
931
+ b"'",
932
+ b"&apos;",
585
933
  )
586
934
  new_contents = re.sub(pattern, replacement, new_contents)
587
935
 
@@ -600,7 +948,7 @@ class XML:
600
948
  # this is not using xpath - do a simple search and replace
601
949
  else:
602
950
  logger.debug("Replacement without xpath...")
603
- with open(file_path, "r", encoding="UTF-8") as f:
951
+ with open(file_path, encoding="UTF-8") as f:
604
952
  contents = f.read()
605
953
  # Replace all occurrences of the search pattern with the replace string
606
954
  new_contents = pattern.sub(replace_string, contents)
@@ -626,15 +974,22 @@ class XML:
626
974
  cls,
627
975
  directory: str,
628
976
  xpath: str,
977
+ logger: logging.Logger = default_logger,
629
978
  ) -> list | None:
630
- """Extracts the XML subtrees using an XPath in all XML files
631
- in the directory and its subdirectories.
979
+ """Extract the XML subtrees using an XPath in all XML files in the directory and its subdirectories.
632
980
 
633
981
  Args:
634
- directory (str): directory to traverse for XML files
635
- xpath (str): used to determine XML elements to extract
982
+ directory (str):
983
+ The directory to traverse for XML files.
984
+ xpath (str):
985
+ Used to determine XML elements to extract.
986
+ logger (logging.Logger):
987
+ The logging object used for all log messages.
988
+
636
989
  Returns:
637
- list | None: Extracted data if it is found by the XPath, None otherwise
990
+ list | None:
991
+ Extracted data if it is found by the XPath, None otherwise.
992
+
638
993
  """
639
994
 
640
995
  extracted_data_list = []
@@ -651,7 +1006,8 @@ class XML:
651
1006
  tree = etree.parse(file_path)
652
1007
  if not tree:
653
1008
  logger.error(
654
- "Cannot parse XML file -> '%s'. Skipping...", file_path
1009
+ "Cannot parse XML file -> '%s'. Skipping...",
1010
+ file_path,
655
1011
  )
656
1012
  continue
657
1013
  root = tree.getroot()