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.
- pyxecm/__init__.py +6 -4
- pyxecm/avts.py +673 -246
- pyxecm/coreshare.py +686 -467
- pyxecm/customizer/__init__.py +16 -4
- pyxecm/customizer/__main__.py +58 -0
- pyxecm/customizer/api/__init__.py +5 -0
- pyxecm/customizer/api/__main__.py +6 -0
- pyxecm/customizer/api/app.py +914 -0
- pyxecm/customizer/api/auth.py +154 -0
- pyxecm/customizer/api/metrics.py +92 -0
- pyxecm/customizer/api/models.py +13 -0
- pyxecm/customizer/api/payload_list.py +865 -0
- pyxecm/customizer/api/settings.py +103 -0
- pyxecm/customizer/browser_automation.py +332 -139
- pyxecm/customizer/customizer.py +1007 -1130
- pyxecm/customizer/exceptions.py +35 -0
- pyxecm/customizer/guidewire.py +322 -0
- pyxecm/customizer/k8s.py +713 -378
- pyxecm/customizer/log.py +107 -0
- pyxecm/customizer/m365.py +2867 -909
- pyxecm/customizer/nhc.py +1169 -0
- pyxecm/customizer/openapi.py +258 -0
- pyxecm/customizer/payload.py +16817 -7467
- pyxecm/customizer/pht.py +699 -285
- pyxecm/customizer/salesforce.py +516 -342
- pyxecm/customizer/sap.py +58 -41
- pyxecm/customizer/servicenow.py +593 -371
- pyxecm/customizer/settings.py +442 -0
- pyxecm/customizer/successfactors.py +408 -346
- pyxecm/customizer/translate.py +83 -48
- pyxecm/helper/__init__.py +5 -2
- pyxecm/helper/assoc.py +83 -43
- pyxecm/helper/data.py +2406 -870
- pyxecm/helper/logadapter.py +27 -0
- pyxecm/helper/web.py +229 -101
- pyxecm/helper/xml.py +527 -171
- pyxecm/maintenance_page/__init__.py +5 -0
- pyxecm/maintenance_page/__main__.py +6 -0
- pyxecm/maintenance_page/app.py +51 -0
- pyxecm/maintenance_page/settings.py +28 -0
- pyxecm/maintenance_page/static/favicon.avif +0 -0
- pyxecm/maintenance_page/templates/maintenance.html +165 -0
- pyxecm/otac.py +234 -140
- pyxecm/otawp.py +1436 -557
- pyxecm/otcs.py +7716 -3161
- pyxecm/otds.py +2150 -919
- pyxecm/otiv.py +36 -21
- pyxecm/otmm.py +1272 -325
- pyxecm/otpd.py +231 -127
- pyxecm-2.0.0.dist-info/METADATA +145 -0
- pyxecm-2.0.0.dist-info/RECORD +54 -0
- {pyxecm-1.6.dist-info → pyxecm-2.0.0.dist-info}/WHEEL +1 -1
- pyxecm-1.6.dist-info/METADATA +0 -53
- pyxecm-1.6.dist-info/RECORD +0 -32
- {pyxecm-1.6.dist-info → pyxecm-2.0.0.dist-info/licenses}/LICENSE +0 -0
- {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
|
-
"""
|
|
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
|
|
22
|
+
from lxml.etree import Element
|
|
34
23
|
|
|
35
|
-
|
|
36
|
-
from pyxecm.helper.assoc import Assoc
|
|
24
|
+
from pyxecm.helper import Assoc
|
|
37
25
|
|
|
38
|
-
|
|
26
|
+
default_logger = logging.getLogger("pyxecm.helper.xml")
|
|
39
27
|
|
|
40
28
|
|
|
41
29
|
class XML:
|
|
42
|
-
"""
|
|
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,
|
|
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):
|
|
52
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
elements
|
|
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
|
|
79
|
-
logger.error("IO Error -> %s",
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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,
|
|
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
|
-
|
|
95
|
-
|
|
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):
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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:
|
|
133
|
+
list:
|
|
134
|
+
List of dictionaries.
|
|
135
|
+
|
|
106
136
|
"""
|
|
107
137
|
|
|
108
|
-
|
|
138
|
+
if not filenames:
|
|
139
|
+
filenames = ["*.xml"]
|
|
109
140
|
|
|
110
|
-
|
|
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
|
|
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
|
-
|
|
127
|
-
|
|
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",
|
|
194
|
+
"Load XML file -> '%s' of size -> %s",
|
|
195
|
+
file_path,
|
|
196
|
+
file_size,
|
|
150
197
|
)
|
|
151
|
-
|
|
152
|
-
file_path,
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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(
|
|
168
|
-
|
|
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):
|
|
172
|
-
|
|
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
|
-
|
|
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(
|
|
190
|
-
|
|
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):
|
|
194
|
-
|
|
195
|
-
|
|
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):
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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:
|
|
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
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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):
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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:
|
|
626
|
+
str:
|
|
627
|
+
The updated element text.
|
|
628
|
+
|
|
290
629
|
"""
|
|
291
630
|
|
|
292
631
|
if is_simple:
|
|
293
|
-
if is_escaped:
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
320
|
-
|
|
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):
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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:
|
|
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...",
|
|
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,
|
|
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
|
-
|
|
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",
|
|
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",
|
|
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,
|
|
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",
|
|
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
|
-
"'",
|
|
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
|
-
"'",
|
|
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",
|
|
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",
|
|
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""",
|
|
901
|
+
b""",
|
|
902
|
+
b""",
|
|
558
903
|
)
|
|
559
904
|
new_contents = new_contents.replace(
|
|
560
|
-
b"'",
|
|
905
|
+
b"'",
|
|
906
|
+
b"'",
|
|
561
907
|
)
|
|
562
908
|
new_contents = new_contents.replace(b">", b">")
|
|
563
909
|
new_contents = new_contents.replace(b"&lt;", 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'"',
|
|
925
|
+
replacement = lambda match: match.group(0).replace( # noqa: E731
|
|
926
|
+
b'"',
|
|
927
|
+
b""",
|
|
581
928
|
)
|
|
582
929
|
new_contents = re.sub(pattern, replacement, new_contents)
|
|
583
|
-
replacement = lambda match: match.group(0).replace(
|
|
584
|
-
b"'",
|
|
930
|
+
replacement = lambda match: match.group(0).replace( # noqa: E731
|
|
931
|
+
b"'",
|
|
932
|
+
b"'",
|
|
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,
|
|
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
|
-
"""
|
|
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):
|
|
635
|
-
|
|
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:
|
|
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...",
|
|
1009
|
+
"Cannot parse XML file -> '%s'. Skipping...",
|
|
1010
|
+
file_path,
|
|
655
1011
|
)
|
|
656
1012
|
continue
|
|
657
1013
|
root = tree.getroot()
|