pyxecm 1.4__py3-none-any.whl → 1.5__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/helper/web.py CHANGED
@@ -20,6 +20,7 @@ import logging
20
20
  import socket
21
21
  import time
22
22
  import requests
23
+ from lxml import html
23
24
 
24
25
  logger = logging.getLogger("pyxecm.web")
25
26
 
@@ -47,7 +48,7 @@ class HTTP(object):
47
48
  bool: True is reachable, False otherwise
48
49
  """
49
50
 
50
- logger.info(
51
+ logger.debug(
51
52
  "Test if host -> %s is reachable on port -> %s ...", hostname, str(port)
52
53
  )
53
54
  try:
@@ -67,7 +68,7 @@ class HTTP(object):
67
68
  )
68
69
  return False
69
70
  else:
70
- logger.info("Host is reachable at -> %s:%s", hostname, str(port))
71
+ logger.debug("Host is reachable at -> %s:%s", hostname, str(port))
71
72
  return True
72
73
 
73
74
  # end method definition
@@ -81,6 +82,8 @@ class HTTP(object):
81
82
  timeout: int = 60,
82
83
  retries: int = 0,
83
84
  wait_time: int = 0,
85
+ wait_on_status: list | None = None,
86
+ show_error: bool = True,
84
87
  ):
85
88
  """Issues an http request to a given URL.
86
89
 
@@ -93,6 +96,9 @@ class HTTP(object):
93
96
  timeout (int, optional): timeout in seconds
94
97
  retries (int, optional): number of retries. If -1 then unlimited retries.
95
98
  wait_time (int, optional): number of seconds to wait after each try
99
+ wait_on_status (list, optional): list of status codes we want to wait on. If None
100
+ or empty then we wait for all return codes if
101
+ wait_time > 0
96
102
  Returns:
97
103
  Response of call
98
104
  """
@@ -100,61 +106,179 @@ class HTTP(object):
100
106
  if not headers:
101
107
  headers = requestHeaders
102
108
 
103
- logger.info(
104
- "Make HTTP Request to URL -> %s using -> %s method with payload -> %s (max number of retries = %s)",
105
- url,
106
- method,
107
- str(payload),
108
- str(retries),
109
+ message = "Make HTTP Request to URL -> {} using -> {} method".format(
110
+ url, method
109
111
  )
112
+ if payload:
113
+ message += " with payload -> {}".format(payload)
114
+ if retries:
115
+ message += " (max number of retries -> {}, wait time between retries -> {})".format(
116
+ retries, wait_time
117
+ )
118
+ try:
119
+ retries = int(retries)
120
+ except ValueError:
121
+ logger.warning(
122
+ "HTTP request -> retries is not a valid integer value: %s, defaulting to 0 retries ",
123
+ retries,
124
+ )
125
+ retries = 0
126
+
127
+ logger.debug(message)
110
128
 
111
129
  try_counter = 1
112
130
 
113
131
  while True:
114
- response = requests.request(
115
- method=method, url=url, data=payload, headers=headers, timeout=timeout
116
- )
117
-
118
- if not response.ok and retries == 0:
119
- logger.error(
120
- "HTTP request -> %s to url -> %s failed; status -> %s; error -> %s",
121
- method,
122
- url,
123
- response.status_code,
124
- response.text,
132
+ try:
133
+ response = requests.request(
134
+ method=method,
135
+ url=url,
136
+ data=payload,
137
+ headers=headers,
138
+ timeout=timeout,
125
139
  )
126
- return response
127
-
128
- elif response.ok:
129
- logger.info(
130
- "HTTP request -> %s to url -> %s succeeded with status -> %s!",
131
- method,
132
- url,
133
- response.status_code,
134
- )
135
- if wait_time > 0:
136
- logger.info("Sleeping %s seconds...", wait_time)
137
- time.sleep(wait_time)
138
- return response
139
-
140
- else:
140
+ logger.debug("%s", response.text)
141
+ except Exception as exc:
142
+ response = None
141
143
  logger.warning(
142
- "HTTP request -> %s to url -> %s failed (try %s); status -> %s; error -> %s",
144
+ "HTTP request -> %s to url -> %s failed failed (try %s); error -> %s",
143
145
  method,
144
146
  url,
145
147
  try_counter,
146
- response.status_code,
147
- response.text,
148
+ exc,
148
149
  )
149
- if wait_time > 0:
150
- logger.warning(
151
- "Sleeping %s seconds and then trying once more...",
152
- str(wait_time),
150
+
151
+ # do we have an error and don't want to retry?
152
+ if response is not None:
153
+ # Do we have a successful result?
154
+ if response.ok:
155
+ logger.debug(
156
+ "HTTP request -> %s to url -> %s succeeded with status -> %s!",
157
+ method,
158
+ url,
159
+ response.status_code,
153
160
  )
154
- time.sleep(wait_time)
155
- else:
156
- logger.warning("Trying once more...")
157
- retries -= 1
158
- try_counter += 1
161
+
162
+ if wait_on_status and response.status_code in wait_on_status:
163
+ logger.debug(
164
+ "%s is in wait_on_status list: %s",
165
+ response.status_code,
166
+ wait_on_status,
167
+ )
168
+ else:
169
+ return response
170
+
171
+ elif not response.ok:
172
+ message = "HTTP request -> {} to url -> {} failed; status -> {}; error -> {}".format(
173
+ method,
174
+ url,
175
+ response.status_code,
176
+ (
177
+ response.text
178
+ if response.headers.get("content-type")
179
+ == "application/json"
180
+ else "see debug log"
181
+ ),
182
+ )
183
+ if show_error and retries == 0:
184
+ logger.error(message)
185
+ else:
186
+ logger.warning(message)
187
+
188
+ # Check if another retry is allowed, if not return None
189
+ if retries == 0:
190
+ return None
191
+
192
+ if wait_time > 0:
193
+ logger.warning(
194
+ "Sleeping %s seconds and then trying once more...",
195
+ str(wait_time),
196
+ )
197
+ time.sleep(wait_time)
198
+
199
+ retries -= 1
200
+ try_counter += 1
201
+
202
+ # end method definition
203
+
204
+ def download_file(
205
+ self,
206
+ url: str,
207
+ filename: str,
208
+ timeout: int = 120,
209
+ retries: int = 0,
210
+ wait_time: int = 0,
211
+ wait_on_status: list | None = None,
212
+ show_error: bool = True,
213
+ ) -> bool:
214
+ """Download a file from a URL
215
+
216
+ Args:
217
+ url (str): URL
218
+ filename (str): filename to save
219
+ timeout (int, optional): timeout in seconds
220
+ retries (int, optional): number of retries. If -1 then unlimited retries.
221
+ wait_time (int, optional): number of seconds to wait after each try
222
+ wait_on_status (list, optional): list of status codes we want to wait on. If None
223
+ or empty then we wait for all return codes if
224
+ wait_time > 0
225
+
226
+ Returns:
227
+ bool: True if successful, False otherwise
228
+ """
229
+
230
+ response = self.http_request(
231
+ url=url,
232
+ method="GET",
233
+ retries=retries,
234
+ timeout=timeout,
235
+ wait_time=wait_time,
236
+ wait_on_status=wait_on_status,
237
+ show_error=show_error,
238
+ )
239
+
240
+ if response is None:
241
+ return False
242
+
243
+ if response.ok:
244
+ with open(filename, "wb") as f:
245
+ f.write(response.content)
246
+ logger.debug("File downloaded successfully as -> %s", filename)
247
+ return True
248
+
249
+ return False
159
250
 
160
251
  # end method definition
252
+
253
+ def extract_content(self, url: str, xpath: str) -> str | None:
254
+ """Extract a string from a response of a HTTP request
255
+ based on an XPath.
256
+
257
+ Args:
258
+ url (str): URL to open
259
+ xpath (str): XPath expression to apply to the result
260
+
261
+ Returns:
262
+ str | None: Extracted string or None in case of an error.
263
+ """
264
+
265
+ # Send a GET request to the URL
266
+ response = requests.get(url, timeout=None)
267
+
268
+ # Check if request was successful
269
+ if response.status_code == 200:
270
+ # Parse the HTML content
271
+ tree = html.fromstring(response.content)
272
+
273
+ # Extract content using XPath
274
+ elements = tree.xpath(xpath)
275
+
276
+ # Get text content of all elements and join them
277
+ content = "\n".join([elem.text_content().strip() for elem in elements])
278
+
279
+ # Return the extracted content
280
+ return content
281
+ else:
282
+ # If request was not successful, print error message
283
+ logger.error(response.status_code)
284
+ return None
pyxecm/helper/xml.py CHANGED
@@ -3,6 +3,10 @@
3
3
  Class: XML
4
4
  Methods:
5
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.
6
10
  get_xml_element: Retrieve an XML Element from a string using an XPath expression
7
11
  modify_xml_element: Update the text (= content) of an XML element
8
12
  search_setting: Search a JSON-like setting inside an XML text telement
@@ -21,10 +25,12 @@ __email__ = "mdiefenb@opentext.com"
21
25
  import logging
22
26
  import os
23
27
  import re
28
+ import fnmatch
24
29
 
25
30
  # we need lxml instead of stadard xml.etree to have xpath capabilities!
26
31
  from lxml import etree
27
32
  import xmltodict
33
+ import zipfile
28
34
 
29
35
  # import xml.etree.ElementTree as etree
30
36
  from pyxecm.helper.assoc import Assoc
@@ -33,7 +39,129 @@ logger = logging.getLogger("pyxecm.xml")
33
39
 
34
40
 
35
41
  class XML:
36
- """XML Class to parse and update Extended ECM transport packages"""
42
+ """XML Class to handle XML processing, e.g. to parse and update Extended ECM transport packages"""
43
+
44
+ @classmethod
45
+ def load_xml_file(
46
+ cls, file_path: str, xpath: str, dir_name: str | None = None
47
+ ) -> list | None:
48
+ """Load an XML file into a Python list of dictionaries
49
+
50
+ Args:
51
+ file_path (str): Path to XML file
52
+ xpath (str): XPath to select sub-elements
53
+
54
+ Returns:
55
+ dict | None: _description_
56
+ """
57
+
58
+ try:
59
+
60
+ tree = etree.parse(file_path)
61
+ if not tree:
62
+ return []
63
+
64
+ # Perform the XPath query to select 'child' elements
65
+ elements = tree.xpath(xpath) # Adjust XPath as needed
66
+
67
+ # Convert the selected elements to dictionaries
68
+ results = []
69
+ tag = xpath.split("/")[-1]
70
+ for element in elements:
71
+ element_dict = xmltodict.parse(etree.tostring(element))
72
+ if tag in element_dict:
73
+ element_dict = element_dict[tag]
74
+ if dir_name:
75
+ element_dict["directory"] = dir_name
76
+ results.append(element_dict)
77
+
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))
84
+
85
+ return results
86
+
87
+ # end method definition
88
+
89
+ @classmethod
90
+ def load_xml_files_from_directory(
91
+ cls, path_to_root: str, filenames: list | None, xpath: str | None = None
92
+ ) -> 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.
96
+
97
+ 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
103
+
104
+ Returns:
105
+ list: List of dictionaries
106
+ """
107
+
108
+ try:
109
+
110
+ # Check if the provided path is a directory
111
+ if not os.path.isdir(path_to_root) and not path_to_root.endswith(".zip"):
112
+ logger.error(
113
+ "The provided path '%s' is not a valid directory or Zip file.",
114
+ path_to_root,
115
+ )
116
+ return False
117
+
118
+ if path_to_root.endswith(".zip"):
119
+ zip_file_folder = os.path.splitext(path_to_root)[0]
120
+ if not os.path.exists(zip_file_folder):
121
+ logger.info(
122
+ "Unzipping -> '%s' into folder -> '%s'...",
123
+ path_to_root,
124
+ zip_file_folder,
125
+ )
126
+ with zipfile.ZipFile(path_to_root, "r") as zfile:
127
+ zfile.extractall(zip_file_folder)
128
+ else:
129
+ logger.info(
130
+ "Zip file is already extracted (path -> '%s' exists). Reusing extracted data...",
131
+ zip_file_folder,
132
+ )
133
+ path_to_root = zip_file_folder
134
+
135
+ results = []
136
+
137
+ # Walk through the directory
138
+ for root, _, files in os.walk(path_to_root):
139
+ for file_data in files:
140
+ file_path = os.path.join(root, file_data)
141
+ file_size = os.path.getsize(file_path)
142
+ file_name = os.path.basename(file_path)
143
+ dir_name = os.path.dirname(file_path)
144
+
145
+ if any(
146
+ fnmatch.fnmatch(file_path, pattern) for pattern in filenames
147
+ ) and file_name.endswith(".xml"):
148
+ logger.info(
149
+ "Load XML file -> '%s' of size -> %s", file_path, file_size
150
+ )
151
+ results += cls.load_xml_file(
152
+ file_path, xpath=xpath, dir_name=dir_name
153
+ )
154
+
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))
161
+
162
+ return results
163
+
164
+ # end method definition
37
165
 
38
166
  @classmethod
39
167
  def get_xml_element(cls, xml_content: str, xpath: str):
@@ -55,6 +183,8 @@ class XML:
55
183
 
56
184
  return element
57
185
 
186
+ # end method definition
187
+
58
188
  @classmethod
59
189
  def modify_xml_element(cls, xml_content: str, xpath: str, new_value: str):
60
190
  """Update the text (= content) of an XML element
@@ -72,6 +202,8 @@ class XML:
72
202
  else:
73
203
  logger.warning("XML Element -> %s not found.", xpath)
74
204
 
205
+ # end method definition
206
+
75
207
  @classmethod
76
208
  def search_setting(
77
209
  cls,
@@ -122,6 +254,8 @@ class XML:
122
254
  else:
123
255
  return None
124
256
 
257
+ # end method definition
258
+
125
259
  @classmethod
126
260
  def replace_setting(
127
261
  cls,
@@ -170,6 +304,8 @@ class XML:
170
304
 
171
305
  return new_text
172
306
 
307
+ # end method definition
308
+
173
309
  @classmethod
174
310
  def replace_in_xml_files(
175
311
  cls,
@@ -216,8 +352,8 @@ class XML:
216
352
  # if xpath is given we do an intelligent replacement
217
353
  if xpath:
218
354
  xml_modified = False
219
- logger.info("Replacement with xpath...")
220
- logger.info(
355
+ logger.debug("Replacement with xpath...")
356
+ logger.debug(
221
357
  "XML path -> %s, setting -> %s, assoc element -> %s",
222
358
  xpath,
223
359
  setting,
@@ -225,17 +361,15 @@ class XML:
225
361
  )
226
362
  tree = etree.parse(file_path)
227
363
  if not tree:
228
- logger.erro(
229
- "Cannot parse XML tree -> {}. Skipping...".format(
230
- file_path
231
- )
364
+ logger.error(
365
+ "Cannot parse XML tree -> %s. Skipping...", file_path
232
366
  )
233
367
  continue
234
368
  root = tree.getroot()
235
369
  # find the matching XML elements using the given XPath:
236
370
  elements = root.xpath(xpath)
237
371
  if not elements:
238
- logger.info(
372
+ logger.debug(
239
373
  "The XML file -> %s does not have any element with the given XML path -> %s. Skipping...",
240
374
  file_path,
241
375
  xpath,
@@ -243,7 +377,7 @@ class XML:
243
377
  continue
244
378
  for element in elements:
245
379
  # as XPath returns a list
246
- logger.info(
380
+ logger.debug(
247
381
  "Found XML element -> %s in file -> %s using xpath -> %s",
248
382
  element.tag,
249
383
  filename,
@@ -251,7 +385,7 @@ class XML:
251
385
  )
252
386
  # the simple case: replace the complete text of the XML element
253
387
  if not setting and not assoc_elem:
254
- logger.info(
388
+ logger.debug(
255
389
  "Replace complete text of XML element -> %s from -> %s to -> %s",
256
390
  xpath,
257
391
  element.text,
@@ -261,7 +395,7 @@ class XML:
261
395
  xml_modified = True
262
396
  # In this case we want to set a complete value of a setting (basically replacing a whole line)
263
397
  elif setting and not assoc_elem:
264
- logger.info(
398
+ logger.debug(
265
399
  "Replace single setting -> %s in XML element -> %s with new value -> %s",
266
400
  setting,
267
401
  xpath,
@@ -271,7 +405,7 @@ class XML:
271
405
  element.text, setting, is_simple=True
272
406
  )
273
407
  if setting_value:
274
- logger.info(
408
+ logger.debug(
275
409
  "Found existing setting value -> %s",
276
410
  setting_value,
277
411
  )
@@ -290,7 +424,7 @@ class XML:
290
424
  replace_setting = (
291
425
  '"' + setting + '":"' + replace_string + '"'
292
426
  )
293
- logger.info(
427
+ logger.debug(
294
428
  "Replacement setting -> %s", replace_setting
295
429
  )
296
430
  element.text = cls.replace_setting(
@@ -308,7 +442,7 @@ class XML:
308
442
  continue
309
443
  # in this case the text is just one assoc (no setting substructure)
310
444
  elif not setting and assoc_elem:
311
- logger.info(
445
+ logger.debug(
312
446
  "Replace single Assoc value -> %s in XML element -> %s with -> %s",
313
447
  assoc_elem,
314
448
  xpath,
@@ -322,13 +456,13 @@ class XML:
322
456
  assoc_string=assoc_string
323
457
  )
324
458
  logger.debug("Assoc Dict -> %s", str(assoc_dict))
325
- assoc_dict[
326
- assoc_elem
327
- ] = replace_string # escaped_replace_string
459
+ assoc_dict[assoc_elem] = (
460
+ replace_string # escaped_replace_string
461
+ )
328
462
  assoc_string_new: str = Assoc.dict_to_string(
329
463
  assoc_dict=assoc_dict
330
464
  )
331
- logger.info(
465
+ logger.debug(
332
466
  "Replace assoc with -> %s", assoc_string_new
333
467
  )
334
468
  element.text = assoc_string_new
@@ -336,7 +470,7 @@ class XML:
336
470
  xml_modified = True
337
471
  # In this case we have multiple settings with their own assocs
338
472
  elif setting and assoc_elem:
339
- logger.info(
473
+ logger.debug(
340
474
  "Replace single Assoc value -> %s in setting -> %s in XML element -> %s with -> %s",
341
475
  assoc_elem,
342
476
  setting,
@@ -347,7 +481,7 @@ class XML:
347
481
  element.text, setting, is_simple=False
348
482
  )
349
483
  if setting_value:
350
- logger.info(
484
+ logger.debug(
351
485
  "Found setting value -> %s", setting_value
352
486
  )
353
487
  assoc_string: str = Assoc.extract_assoc_string(
@@ -361,13 +495,13 @@ class XML:
361
495
  escaped_replace_string = replace_string.replace(
362
496
  "'", "\\\\\u0027"
363
497
  )
364
- logger.info(
498
+ logger.debug(
365
499
  "Escaped replacement string -> %s",
366
500
  escaped_replace_string,
367
501
  )
368
- assoc_dict[
369
- assoc_elem
370
- ] = escaped_replace_string # escaped_replace_string
502
+ assoc_dict[assoc_elem] = (
503
+ escaped_replace_string # escaped_replace_string
504
+ )
371
505
  assoc_string_new: str = Assoc.dict_to_string(
372
506
  assoc_dict=assoc_dict
373
507
  )
@@ -378,7 +512,7 @@ class XML:
378
512
  replace_setting = (
379
513
  '"' + setting + '":"' + assoc_string_new + '"'
380
514
  )
381
- logger.info(
515
+ logger.debug(
382
516
  "Replacement setting -> %s", replace_setting
383
517
  )
384
518
  # here we need to apply a "trick". It is required
@@ -407,7 +541,7 @@ class XML:
407
541
  )
408
542
  continue
409
543
  if xml_modified:
410
- logger.info(
544
+ logger.debug(
411
545
  "XML tree has been modified. Write updated file -> %s...",
412
546
  file_path,
413
547
  )
@@ -465,7 +599,7 @@ class XML:
465
599
  found = True
466
600
  # this is not using xpath - do a simple search and replace
467
601
  else:
468
- logger.info("Replacement without xpath...")
602
+ logger.debug("Replacement without xpath...")
469
603
  with open(file_path, "r", encoding="UTF-8") as f:
470
604
  contents = f.read()
471
605
  # Replace all occurrences of the search pattern with the replace string
@@ -473,7 +607,7 @@ class XML:
473
607
 
474
608
  # Write the updated contents to the file if there were replacements
475
609
  if contents != new_contents:
476
- logger.info(
610
+ logger.debug(
477
611
  "Found search string -> %s in XML file -> %s. Write updated file...",
478
612
  search_pattern,
479
613
  file_path,
@@ -485,6 +619,8 @@ class XML:
485
619
 
486
620
  return found
487
621
 
622
+ # end method definition
623
+
488
624
  @classmethod
489
625
  def extract_from_xml_files(
490
626
  cls,
@@ -511,18 +647,18 @@ class XML:
511
647
  # Read the contents of the file
512
648
  file_path = os.path.join(subdir, filename)
513
649
 
514
- logger.info("Extraction with xpath -> %s...", xpath)
650
+ logger.debug("Extraction with xpath -> %s...", xpath)
515
651
  tree = etree.parse(file_path)
516
652
  if not tree:
517
- logger.erro(
518
- "Cannot parse XML tree -> {}. Skipping...".format(file_path)
653
+ logger.error(
654
+ "Cannot parse XML file -> '%s'. Skipping...", file_path
519
655
  )
520
656
  continue
521
657
  root = tree.getroot()
522
658
  # find the matching XML elements using the given XPath:
523
659
  elements = root.xpath(xpath)
524
660
  if not elements:
525
- logger.info(
661
+ logger.debug(
526
662
  "The XML file -> %s does not have any element with the given XML path -> %s. Skipping...",
527
663
  file_path,
528
664
  xpath,
@@ -530,7 +666,7 @@ class XML:
530
666
  continue
531
667
  for element in elements:
532
668
  # as XPath returns a list
533
- logger.info(
669
+ logger.debug(
534
670
  "Found XML element -> %s in file -> %s using xpath -> %s. Add it to result list.",
535
671
  element.tag,
536
672
  filename,
@@ -551,4 +687,4 @@ class XML:
551
687
 
552
688
  return extracted_data_list
553
689
 
554
- # end method definition
690
+ # end method definition