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
@@ -1,81 +1,118 @@
1
- """
2
- browser_automation Module to implement a class to automate configuration
3
- via a browser interface. These are typically used as fallback options if
4
- no REST API or LLConfig can be used.
5
-
6
- Class: BrowserAutomation
7
- Methods:
8
-
9
- __init__ : class initializer. Start the browser session.
10
- set_chrome_options: Sets chrome options for Selenium. Chrome options for headless browser is enabled
11
- get_page: Load a page into the browser based on a given URL.
12
- find_elem: Find an page element
13
- find_elem_and_click: Find an page element and click it
14
- find_elem_and_set: Find an page element and fill it with a new text.
15
- find_element_and_download: Clicks a page element to initiate a download.
16
- run_login: Login to target system via the browser
17
- implicit_wait: Waits for the browser to finish tasks (e.g. fully loading a page).
18
- This setting is valid for the whole browser session.
19
- See https://www.selenium.dev/documentation/webdriver/waits/
20
- end_session: End the browser session
1
+ """browser_automation Module to automate configuration via a browser interface.
2
+
3
+ These are typically used as fallback options if no REST API or LLConfig can be used.
21
4
  """
22
5
 
23
- import os
6
+ __author__ = "Dr. Marc Diefenbruch"
7
+ __copyright__ = "Copyright 2025, OpenText"
8
+ __credits__ = ["Kai-Philip Gatzweiler"]
9
+ __maintainer__ = "Dr. Marc Diefenbruch"
10
+ __email__ = "mdiefenb@opentext.com"
11
+
12
+
24
13
  import logging
14
+ import os
15
+ import tempfile
16
+ import time
17
+
18
+ import urllib3
25
19
 
26
- logger = logging.getLogger("pyxecm.customizer.browser_automation")
20
+ default_logger = logging.getLogger("pyxecm.customizer.browser_automation")
27
21
 
28
22
  # For backwards compatibility we also want to handle
29
23
  # cases where the selenium and chromedriver_autoinstaller
30
24
  # modules have not been installed in the customizer container:
31
25
  try:
32
- from selenium.webdriver.chrome.options import Options
33
26
  from selenium import webdriver
34
- from selenium.webdriver.common.by import By
35
- from selenium.webdriver.common.action_chains import ActionChains
36
- from selenium.webdriver.remote.webelement import WebElement
37
27
  from selenium.common.exceptions import (
38
- WebDriverException,
39
- NoSuchElementException,
40
- ElementNotInteractableException,
41
28
  ElementClickInterceptedException,
42
- TimeoutException,
29
+ ElementNotInteractableException,
30
+ InvalidElementStateException,
43
31
  MoveTargetOutOfBoundsException,
32
+ NoSuchElementException,
33
+ StaleElementReferenceException,
34
+ TimeoutException,
35
+ WebDriverException,
44
36
  )
37
+ from selenium.webdriver.chrome.options import Options
38
+ from selenium.webdriver.common.action_chains import ActionChains
39
+ from selenium.webdriver.common.by import By
40
+ from selenium.webdriver.remote.webelement import WebElement
41
+ from selenium.webdriver.support.ui import Select
45
42
 
46
- except ModuleNotFoundError as module_exception:
47
- logger.warning("Module selenium is not installed")
43
+ except ModuleNotFoundError:
44
+ default_logger.warning("Module selenium is not installed")
48
45
 
49
46
  class Options:
50
- """Dummy class to avoid errors if selenium module cannot be imported"""
47
+ """Dummy class to avoid errors if selenium module cannot be imported."""
51
48
 
52
49
  class By:
53
- """Dummy class to avoid errors if selenium module cannot be imported"""
50
+ """Dummy class to avoid errors if selenium module cannot be imported."""
54
51
 
55
52
  ID: str = ""
56
53
 
57
54
  class WebElement:
58
- """Dummy class to avoid errors if selenium module cannot be imported"""
55
+ """Dummy class to avoid errors if selenium module cannot be imported."""
59
56
 
60
57
 
61
58
  try:
62
59
  import chromedriver_autoinstaller
63
- except ModuleNotFoundError as module_exception:
64
- logger.warning("Module chromedriver_autoinstaller is not installed!")
60
+ except ModuleNotFoundError:
61
+ default_logger.warning("Module chromedriver_autoinstaller is not installed!")
65
62
 
66
63
 
67
64
  class BrowserAutomation:
68
65
  """Class to automate settings via a browser interface."""
69
66
 
67
+ logger: logging.Logger = default_logger
68
+
70
69
  def __init__(
71
70
  self,
72
71
  base_url: str = "",
73
72
  user_name: str = "",
74
73
  user_password: str = "",
75
- download_directory: str = "/tmp",
74
+ download_directory: str | None = None,
76
75
  take_screenshots: bool = False,
77
- automation_name: str = "screen",
76
+ automation_name: str = "",
77
+ logger: logging.Logger = default_logger,
78
78
  ) -> None:
79
+ """Initialize the object.
80
+
81
+ Args:
82
+ base_url (str, optional):
83
+ The base URL of the website to automate. Defaults to "".
84
+ user_name (str, optional): _description_. Defaults to "".
85
+ If an authentication at the web site is required, this is the user name.
86
+ Defaults to "".
87
+ user_password (str, optional):
88
+ If an authentication at the web site is required, this is the user password.
89
+ Defaults to "".
90
+ download_directory (str | None, optional):
91
+ A download directory used for download links. If None,
92
+ a temporary directory is automatically used.
93
+ take_screenshots (bool, optional):
94
+ For debugging purposes, screenshots can be taken.
95
+ Defaults to False.
96
+ automation_name (str, optional):
97
+ The name of the automation. Defaults to "screen".
98
+ logger (logging.Logger, optional):
99
+ The logging object to use for all log messages. Defaults to default_logger.
100
+
101
+ """
102
+
103
+ if not download_directory:
104
+ download_directory = os.path.join(
105
+ tempfile.gettempdir(),
106
+ "browser_automations",
107
+ automation_name,
108
+ "downloads",
109
+ )
110
+
111
+ if logger != default_logger:
112
+ self.logger = logger.getChild("browserautomation")
113
+ for logfilter in logger.filters:
114
+ self.logger.addFilter(logfilter)
115
+
79
116
  self.base_url = base_url
80
117
  self.user_name = user_name
81
118
  self.user_password = user_password
@@ -86,8 +123,11 @@ class BrowserAutomation:
86
123
  self.screenshot_names = automation_name
87
124
  self.screen_counter = 1
88
125
 
89
- self.screenshot_directory = "/tmp/browser_automations/{}".format(
90
- automation_name
126
+ self.screenshot_directory = os.path.join(
127
+ tempfile.gettempdir(),
128
+ "browser_automations",
129
+ automation_name,
130
+ "screenshots",
91
131
  )
92
132
 
93
133
  if self.take_screenshots and not os.path.exists(self.screenshot_directory):
@@ -97,18 +137,27 @@ class BrowserAutomation:
97
137
 
98
138
  # end method definition
99
139
 
100
- def __del__(self):
101
- if self.browser:
102
- self.browser.close()
103
- del self.browser
104
- self.browser = None
140
+ def __del__(self) -> None:
141
+ """Object destructor."""
142
+
143
+ try:
144
+ if self.browser:
145
+ self.browser.quit()
146
+ self.browser = None
147
+ except (WebDriverException, AttributeError, TypeError, OSError):
148
+ # Log or silently handle exceptions during interpreter shutdown
149
+ pass
150
+
151
+ # end method definition
105
152
 
106
153
  def set_chrome_options(self) -> Options:
107
- """Sets chrome options for Selenium.
108
- Chrome options for headless browser is enabled.
154
+ """Set chrome options for Selenium.
155
+
156
+ Chrome options for headless browser is enabled.
109
157
 
110
158
  Returns:
111
159
  Options: Options to call the browser with
160
+
112
161
  """
113
162
 
114
163
  chrome_options = Options()
@@ -117,10 +166,10 @@ class BrowserAutomation:
117
166
  chrome_options.add_argument("--disable-dev-shm-usage")
118
167
  chrome_prefs = {}
119
168
  chrome_options.experimental_options["prefs"] = chrome_prefs
120
- chrome_prefs["profile.default_content_settings"] = {"images": 2}
121
169
 
122
170
  chrome_options.add_experimental_option(
123
- "prefs", {"download.default_directory": self.download_directory}
171
+ "prefs",
172
+ {"download.default_directory": self.download_directory},
124
173
  )
125
174
 
126
175
  return chrome_options
@@ -128,16 +177,20 @@ class BrowserAutomation:
128
177
  # end method definition
129
178
 
130
179
  def take_screenshot(self) -> bool:
131
- """Take a screenshot of the current browser window and save it as PNG file
180
+ """Take a screenshot of the current browser window and save it as PNG file.
132
181
 
133
182
  Returns:
134
- bool: True if successful, False otherwise
183
+ bool:
184
+ True if successful, False otherwise
185
+
135
186
  """
136
187
 
137
188
  screenshot_file = "{}/{}-{}.png".format(
138
- self.screenshot_directory, self.screenshot_names, self.screen_counter
189
+ self.screenshot_directory,
190
+ self.screenshot_names,
191
+ self.screen_counter,
139
192
  )
140
- logger.debug("Save browser screenshot to -> %s", screenshot_file)
193
+ self.logger.debug("Save browser screenshot to -> %s", screenshot_file)
141
194
  result = self.browser.get_screenshot_as_file(screenshot_file)
142
195
  self.screen_counter += 1
143
196
 
@@ -147,68 +200,86 @@ class BrowserAutomation:
147
200
  """Load a page into the browser based on a given URL.
148
201
 
149
202
  Args:
150
- url (str): URL to load. If empty just the base URL will be used
203
+ url (str):
204
+ URL to load. If empty just the base URL will be used
151
205
  Returns:
152
- bool: True if successful, False otherwise
206
+ bool:
207
+ True if successful, False otherwise
208
+
153
209
  """
154
210
 
155
211
  page_url = self.base_url + url
156
212
 
157
213
  try:
158
- logger.debug("Load page -> %s", page_url)
214
+ self.logger.debug("Load page -> %s", page_url)
159
215
  self.browser.get(page_url)
160
- except WebDriverException as exception:
161
- logger.error("Cannot load page -> %s; error -> %s", page_url, exception)
216
+
217
+ except (WebDriverException, urllib3.exceptions.ReadTimeoutError):
218
+ self.logger.error(
219
+ "Cannot load page -> %s!",
220
+ page_url,
221
+ )
162
222
  return False
163
223
 
164
- logger.debug("Page title after get page -> %s", self.browser.title)
224
+ self.logger.debug("Page title after get page -> %s", self.browser.title)
165
225
 
166
226
  if self.take_screenshots:
167
227
  self.take_screenshot()
168
228
 
229
+ # Wait a second before proceeding
230
+ time.sleep(1)
231
+
169
232
  return True
170
233
 
171
234
  # end method definition
172
235
 
173
236
  def get_title(self) -> str:
174
- """Get the browser title. This is handy to validate a certain page is loaded after get_page()
237
+ """Get the browser title.
238
+
239
+ This is handy to validate a certain page is loaded after get_page()
175
240
 
176
241
  Returns:
177
- str: Title of the browser window
242
+ str:
243
+ The title of the browser window.
244
+
178
245
  """
179
246
 
180
247
  if not self.browser:
181
- logger.error("Browser not initialized!")
248
+ self.logger.error("Browser not initialized!")
182
249
  return None
183
250
 
184
251
  return self.browser.title
185
252
 
186
253
  # end method definition
187
254
 
188
- def scroll_to_element(self, element: WebElement):
189
- """Scroll an element into view to make it clickable
255
+ def scroll_to_element(self, element: WebElement) -> None:
256
+ """Scroll an element into view to make it clickable.
190
257
 
191
258
  Args:
192
- element (WebElement): Web element that has been identified before
259
+ element (WebElement):
260
+ Web element that has been identified before.
261
+
193
262
  """
194
263
 
195
264
  if not element:
196
- logger.error("Undefined element!")
265
+ self.logger.error("Undefined element!")
197
266
  return
198
267
 
199
268
  try:
200
269
  actions = ActionChains(self.browser)
201
270
  actions.move_to_element(element).perform()
202
271
  except NoSuchElementException:
203
- logger.error("Element not found in the DOM")
272
+ self.logger.error("Element not found in the DOM!")
204
273
  except TimeoutException:
205
- logger.error("Timed out waiting for the element to be present or visible")
274
+ self.logger.error(
275
+ "Timed out waiting for the element to be present or visible!",
276
+ )
206
277
  except ElementNotInteractableException:
207
- logger.error("Element is not interactable!")
278
+ self.logger.error("Element is not interactable!")
208
279
  except MoveTargetOutOfBoundsException:
209
- logger.error("Element is out of bounds!")
210
- except WebDriverException as e:
211
- logger.error("WebDriverException occurred -> %s", str(e))
280
+ self.logger.error("Element is out of bounds!")
281
+ except WebDriverException:
282
+ self.logger.error("WebDriverException occurred!")
212
283
 
213
284
  # end method definition
214
285
 
@@ -221,11 +292,17 @@ class BrowserAutomation:
221
292
  """Find an page element.
222
293
 
223
294
  Args:
224
- find_elem (str): name of the page element
225
- find_method (str, optional): either By.ID, By.NAME, By.CLASS_NAME, BY.XPATH
226
- show_error (bool, optional): show an error if the element is not found or not clickable
295
+ find_elem (str):
296
+ The name of the page element.
297
+ find_method (str, optional):
298
+ Either By.ID, By.NAME, By.CLASS_NAME, BY.XPATH
299
+ show_error (bool, optional):
300
+ Show an error if the element is not found or not clickable.
301
+
227
302
  Returns:
228
- WebElement: web element or None in case an error occured.
303
+ WebElement:
304
+ The web element or None in case an error occured.
305
+
229
306
  """
230
307
 
231
308
  # We don't want to expose class "By" outside this module,
@@ -239,44 +316,42 @@ class BrowserAutomation:
239
316
  elif find_method == "xpath":
240
317
  find_method = By.XPATH
241
318
  else:
242
- logger.error("Unsupported find method!")
319
+ self.logger.error("Unsupported find method!")
243
320
  return None
244
321
 
245
322
  try:
246
323
  elem = self.browser.find_element(by=find_method, value=find_elem)
247
- except NoSuchElementException as exception:
324
+ except NoSuchElementException:
248
325
  if show_error:
249
- logger.error(
250
- "Cannot find page element -> %s by -> %s; error -> %s",
326
+ self.logger.error(
327
+ "Cannot find page element -> %s by -> %s",
251
328
  find_elem,
252
329
  find_method,
253
- exception,
254
330
  )
255
331
  return None
256
332
  else:
257
- logger.warning(
333
+ self.logger.warning(
258
334
  "Cannot find page element -> %s by -> %s",
259
335
  find_elem,
260
336
  find_method,
261
337
  )
262
338
  return None
263
- except TimeoutException as exception:
264
- logger.error(
265
- "Timed out waiting for the element to be present or visible; error -> %s",
266
- exception,
339
+ except TimeoutException:
340
+ self.logger.error(
341
+ "Timed out waiting for the element to be present or visible!",
267
342
  )
268
343
  return None
269
- except ElementNotInteractableException as exception:
270
- logger.error("Element is not interactable!; error -> %s", exception)
344
+ except ElementNotInteractableException:
345
+ self.logger.error("Element is not interactable!")
271
346
  return None
272
347
  except MoveTargetOutOfBoundsException:
273
- logger.error("Element is out of bounds!")
348
+ self.logger.error("Element is out of bounds!")
274
349
  return None
275
- except WebDriverException as e:
276
- logger.error("WebDriverException occurred -> %s", str(e))
350
+ except WebDriverException:
351
+ self.logger.error("WebDriverException occurred!")
277
352
  return None
278
353
 
279
- logger.debug("Found page element -> %s by -> %s", find_elem, find_method)
354
+ self.logger.debug("Found page element -> %s by -> %s", find_elem, find_method)
280
355
 
281
356
  return elem
282
357
 
@@ -287,52 +362,121 @@ class BrowserAutomation:
287
362
  find_elem: str,
288
363
  find_method: str = By.ID,
289
364
  scroll_to_element: bool = True,
365
+ desired_checkbox_state: bool | None = None,
290
366
  show_error: bool = True,
291
367
  ) -> bool:
292
368
  """Find an page element and click it.
293
369
 
294
370
  Args:
295
- find_elem (str): name of the page element
296
- find_method (str, optional): either By.ID, By.NAME, By.CLASS_NAME, BY.XPATH
297
- scroll_to_element (bool, optional): scroll the element into view
298
- show_error (bool, optional): show an error if the element is not found or not clickable
371
+ find_elem (str):
372
+ The identifier of the page element.
373
+ find_method (str, optional):
374
+ Either By.ID, By.NAME, By.CLASS_NAME, BY.XPATH
375
+ scroll_to_element (bool, optional):
376
+ Scroll the element into view.
377
+ desired_checkbox_state (bool | None, optional):
378
+ If True/False, ensures checkbox matches state.
379
+ If None then click it in any case.
380
+ show_error (bool, optional):
381
+ Show an error if the element is not found or not clickable.
382
+
299
383
  Returns:
300
- bool: True if successful, False otherwise
384
+ bool:
385
+ True if click is successful (or checkbox already in desired state),
386
+ False otherwise.
387
+
301
388
  """
302
389
 
303
390
  if not find_elem:
304
391
  if show_error:
305
- logger.error("Missing element name! Cannot find HTML element!")
392
+ self.logger.error("Missing element name! Cannot find HTML element!")
306
393
  else:
307
- logger.warning("Missing element name! Cannot find HTML element!")
394
+ self.logger.warning("Missing element name! Cannot find HTML element!")
308
395
  return False
309
396
 
310
397
  elem = self.find_elem(
311
- find_elem=find_elem, find_method=find_method, show_error=show_error
398
+ find_elem=find_elem,
399
+ find_method=find_method,
400
+ show_error=show_error,
312
401
  )
313
402
 
314
403
  if not elem:
315
404
  return not show_error
316
405
 
406
+ is_checkbox = elem.get_attribute("type") == "checkbox"
407
+ checkbox_state = None
408
+
317
409
  try:
318
410
  if scroll_to_element:
319
411
  self.scroll_to_element(elem)
320
412
 
413
+ # Handle checkboxes
414
+ if is_checkbox and desired_checkbox_state is not None:
415
+ checkbox_state = elem.is_selected()
416
+ if checkbox_state == desired_checkbox_state:
417
+ self.logger.debug(
418
+ "Checkbox -> '%s' already in desired state -> %s",
419
+ find_elem,
420
+ desired_checkbox_state,
421
+ )
422
+ return True # No need to click
423
+ else:
424
+ self.logger.debug("Checkbox -> '%s' state mismatch. Clicking to change state.", find_elem)
425
+
321
426
  elem.click()
427
+ time.sleep(1)
428
+
429
+ # Handle checkboxes
430
+ if is_checkbox and desired_checkbox_state is not None:
431
+ # Re-locate the element after clicking to avoid stale reference
432
+ elem = self.find_elem(
433
+ find_elem=find_elem,
434
+ find_method=find_method,
435
+ show_error=show_error,
436
+ )
437
+ # Is the element still there?
438
+ if elem:
439
+ checkbox_state = elem.is_selected() if is_checkbox else None
440
+
322
441
  except (
323
442
  ElementClickInterceptedException,
324
443
  ElementNotInteractableException,
325
- ) as exception:
444
+ StaleElementReferenceException,
445
+ InvalidElementStateException,
446
+ ):
326
447
  if show_error:
327
- logger.error(
328
- "Cannot click page element -> %s; error -> %s", find_elem, exception
448
+ self.logger.error(
449
+ "Cannot click page element -> %s!",
450
+ find_elem,
329
451
  )
330
452
  return False
331
453
  else:
332
- logger.warning("Cannot click page element -> %s", find_elem)
454
+ self.logger.warning("Cannot click page element -> %s", find_elem)
333
455
  return True
456
+ except TimeoutException:
457
+ if show_error:
458
+ self.logger.error("Timeout waiting for element -> %s to be clickable!", find_elem)
459
+ return not show_error
334
460
 
335
- logger.debug("Successfully clicked element -> %s", find_elem)
461
+ if checkbox_state is not None:
462
+ if checkbox_state == desired_checkbox_state:
463
+ self.logger.debug(
464
+ "Successfully clicked checkbox element -> %s. It's state is now -> %s",
465
+ find_elem,
466
+ checkbox_state,
467
+ )
468
+ else:
469
+ self.logger.error(
470
+ "Failed to flip checkbox element -> %s to desired state. It's state is still -> %s and not -> %s",
471
+ find_elem,
472
+ checkbox_state,
473
+ desired_checkbox_state,
474
+ )
475
+ else:
476
+ self.logger.debug(
477
+ "Successfully clicked element -> %s",
478
+ find_elem,
479
+ )
336
480
 
337
481
  if self.take_screenshots:
338
482
  self.take_screenshot()
@@ -357,47 +501,78 @@ class BrowserAutomation:
357
501
  is_sensitive (bool, optional): True for suppressing sensitive information in logging
358
502
  Returns:
359
503
  bool: True if successful, False otherwise
504
+
360
505
  """
361
506
 
362
507
  elem = self.find_elem(
363
- find_elem=find_elem, find_method=find_method, show_error=True
508
+ find_elem=find_elem,
509
+ find_method=find_method,
510
+ show_error=True,
364
511
  )
365
512
 
366
513
  if not elem:
367
514
  return False
368
515
 
516
+ if not elem.is_enabled():
517
+ self.logger.error("Cannot set elem -> %s to value -> %s. It is not enabled!", find_elem, elem_value)
518
+ return False
519
+
369
520
  if not is_sensitive:
370
- logger.debug("Set element -> %s to value -> %s...", find_elem, elem_value)
521
+ self.logger.debug(
522
+ "Set element -> %s to value -> %s...",
523
+ find_elem,
524
+ elem_value,
525
+ )
371
526
  else:
372
- logger.debug("Set element -> %s to value -> <sensitive>...", find_elem)
527
+ self.logger.debug("Set element -> %s to value -> <sensitive>...", find_elem)
373
528
 
374
529
  try:
375
- elem.clear() # clear existing text in the input field
376
- elem.send_keys(elem_value) # write new text into the field
377
- except ElementNotInteractableException as exception:
378
- logger.error(
379
- "Cannot set page element -> %s to value -> %s; error -> %s",
530
+ # Check if element is a drop-down (select element)
531
+ if elem.tag_name.lower() == "select":
532
+ select = Select(elem)
533
+ try:
534
+ select.select_by_visible_text(elem_value) # Select option by visible text
535
+ except NoSuchElementException:
536
+ self.logger.error("Option -> '%s' not found in drop-down -> '%s'", elem_value, find_elem)
537
+ return False
538
+ else:
539
+ elem.clear() # clear existing text in the input field
540
+ elem.send_keys(elem_value) # write new text into the field
541
+ except (ElementNotInteractableException, InvalidElementStateException):
542
+ self.logger.error(
543
+ "Cannot set page element -> %s to value -> %s",
380
544
  find_elem,
381
545
  elem_value,
382
- exception,
383
546
  )
384
547
  return False
385
548
 
549
+ if self.take_screenshots:
550
+ self.take_screenshot()
551
+
386
552
  return True
387
553
 
388
554
  # end method definition
389
555
 
390
556
  def find_element_and_download(
391
- self, find_elem: str, find_method: str = By.ID, download_time: int = 30
557
+ self,
558
+ find_elem: str,
559
+ find_method: str = By.ID,
560
+ download_time: int = 30,
392
561
  ) -> str | None:
393
- """Clicks a page element to initiate a download
562
+ """Click a page element to initiate a download.
394
563
 
395
564
  Args:
396
- find_elem (str): page element to click for download
397
- find_method (str, optional): method to find the element. Defaults to By.ID.
398
- download_time (int, optional): time in seconds to wait for the download to complete
565
+ find_elem (str):
566
+ The page element to click for download.
567
+ find_method (str, optional):
568
+ A method to find the element. Defaults to By.ID.
569
+ download_time (int, optional):
570
+ Time in seconds to wait for the download to complete
571
+
399
572
  Returns:
400
- str | None: filename of the download
573
+ str | None:
574
+ The filename of the download.
575
+
401
576
  """
402
577
 
403
578
  # Record the list of files in the download directory before the download
@@ -410,8 +585,6 @@ class BrowserAutomation:
410
585
  return None
411
586
 
412
587
  # Wait for the download to complete
413
- # time.sleep(download_time)
414
-
415
588
  self.browser.implicitly_wait(download_time)
416
589
 
417
590
  # Record the list of files in the download directory after the download
@@ -431,16 +604,32 @@ class BrowserAutomation:
431
604
  login_button: str = "loginbutton",
432
605
  page: str = "",
433
606
  ) -> bool:
434
- """Login to target system via the browser"""
607
+ """Login to target system via the browser.
608
+
609
+ Args:
610
+ user_field (str, optional):
611
+ The name of the web HTML field to enter the user name. Defaults to "otds_username".
612
+ password_field (str, optional):
613
+ The name of the HTML field to enter the password. Defaults to "otds_password".
614
+ login_button (str, optional):
615
+ The name of the HTML login button. Defaults to "loginbutton".
616
+ page (str, optional):
617
+ The URL to the login page. Defaults to "".
618
+
619
+ Returns:
620
+ bool: True = success, False = error.
621
+
622
+ """
435
623
 
436
624
  self.logged_in = False
437
625
 
438
626
  if (
439
627
  not self.get_page(
440
- url=page
628
+ url=page,
441
629
  ) # assuming the base URL leads towards the login page
442
630
  or not self.find_elem_and_set(
443
- find_elem=user_field, elem_value=self.user_name
631
+ find_elem=user_field,
632
+ elem_value=self.user_name,
444
633
  )
445
634
  or not self.find_elem_and_set(
446
635
  find_elem=password_field,
@@ -449,24 +638,24 @@ class BrowserAutomation:
449
638
  )
450
639
  or not self.find_elem_and_click(find_elem=login_button)
451
640
  ):
452
- logger.error(
641
+ self.logger.error(
453
642
  "Cannot log into target system using URL -> %s and user -> %s",
454
643
  self.base_url,
455
644
  self.user_name,
456
645
  )
457
646
  return False
458
647
 
459
- logger.debug("Page title after login -> %s", self.browser.title)
648
+ self.logger.debug("Page title after login -> %s", self.browser.title)
460
649
 
461
650
  # Some special handling for Salesforce login:
462
651
  if "Verify" in self.browser.title:
463
- logger.error(
464
- "Site is asking for a Verification Token. You may need to whitelist your IP!"
652
+ self.logger.error(
653
+ "Site is asking for a Verification Token. You may need to whitelist your IP!",
465
654
  )
466
655
  return False
467
656
  if "Login" in self.browser.title:
468
- logger.error(
469
- "Authentication failed. You may have given the wrong password!"
657
+ self.logger.error(
658
+ "Authentication failed. You may have given the wrong password!",
470
659
  )
471
660
  return False
472
661
 
@@ -476,20 +665,24 @@ class BrowserAutomation:
476
665
 
477
666
  # end method definition
478
667
 
479
- def implicit_wait(self, wait_time: float):
480
- """Waits for the browser to finish tasks (e.g. fully loading a page)
481
- This setting is valid for the whole browser session and not just
482
- for a single command.
668
+ def implicit_wait(self, wait_time: float) -> None:
669
+ """Wait for the browser to finish tasks (e.g. fully loading a page).
670
+
671
+ This setting is valid for the whole browser session and not just
672
+ for a single command.
483
673
 
484
674
  Args:
485
675
  wait_time (float): time in seconds to wait
676
+
486
677
  """
487
678
 
488
- logger.debug("Implicit wait for max -> %s seconds...", str(wait_time))
679
+ self.logger.debug("Implicit wait for max -> %s seconds...", str(wait_time))
489
680
  self.browser.implicitly_wait(wait_time)
490
681
 
491
- def end_session(self):
492
- """End the browser session"""
682
+ # end method definition
683
+
684
+ def end_session(self) -> None:
685
+ """End the browser session. This is just like closing a tab not ending the browser."""
493
686
 
494
687
  self.browser.close()
495
688
  self.logged_in = False