pyxecm 3.0.1__py3-none-any.whl → 3.1.1__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 (52) hide show
  1. pyxecm/avts.py +4 -4
  2. pyxecm/coreshare.py +14 -15
  3. pyxecm/helper/data.py +2 -1
  4. pyxecm/helper/web.py +11 -11
  5. pyxecm/helper/xml.py +41 -10
  6. pyxecm/otac.py +1 -1
  7. pyxecm/otawp.py +19 -19
  8. pyxecm/otca.py +878 -70
  9. pyxecm/otcs.py +1716 -349
  10. pyxecm/otds.py +332 -153
  11. pyxecm/otkd.py +4 -4
  12. pyxecm/otmm.py +1 -1
  13. pyxecm/otpd.py +246 -30
  14. {pyxecm-3.0.1.dist-info → pyxecm-3.1.1.dist-info}/METADATA +2 -1
  15. pyxecm-3.1.1.dist-info/RECORD +82 -0
  16. pyxecm_api/app.py +45 -35
  17. pyxecm_api/auth/functions.py +2 -2
  18. pyxecm_api/auth/router.py +2 -3
  19. pyxecm_api/common/functions.py +67 -12
  20. pyxecm_api/settings.py +0 -8
  21. pyxecm_api/terminal/router.py +1 -1
  22. pyxecm_api/v1_csai/router.py +33 -18
  23. pyxecm_customizer/browser_automation.py +161 -79
  24. pyxecm_customizer/customizer.py +43 -25
  25. pyxecm_customizer/guidewire.py +422 -8
  26. pyxecm_customizer/k8s.py +23 -27
  27. pyxecm_customizer/knowledge_graph.py +498 -20
  28. pyxecm_customizer/m365.py +45 -44
  29. pyxecm_customizer/payload.py +1723 -1188
  30. pyxecm_customizer/payload_list.py +3 -0
  31. pyxecm_customizer/salesforce.py +122 -79
  32. pyxecm_customizer/servicenow.py +27 -7
  33. pyxecm_customizer/settings.py +3 -1
  34. pyxecm_customizer/successfactors.py +2 -2
  35. pyxecm_customizer/translate.py +1 -1
  36. pyxecm-3.0.1.dist-info/RECORD +0 -96
  37. pyxecm_api/agents/__init__.py +0 -7
  38. pyxecm_api/agents/app.py +0 -13
  39. pyxecm_api/agents/functions.py +0 -119
  40. pyxecm_api/agents/models.py +0 -10
  41. pyxecm_api/agents/otcm_knowledgegraph/__init__.py +0 -1
  42. pyxecm_api/agents/otcm_knowledgegraph/functions.py +0 -85
  43. pyxecm_api/agents/otcm_knowledgegraph/models.py +0 -61
  44. pyxecm_api/agents/otcm_knowledgegraph/router.py +0 -74
  45. pyxecm_api/agents/otcm_user_agent/__init__.py +0 -1
  46. pyxecm_api/agents/otcm_user_agent/models.py +0 -20
  47. pyxecm_api/agents/otcm_user_agent/router.py +0 -65
  48. pyxecm_api/agents/otcm_workspace_agent/__init__.py +0 -1
  49. pyxecm_api/agents/otcm_workspace_agent/models.py +0 -40
  50. pyxecm_api/agents/otcm_workspace_agent/router.py +0 -200
  51. {pyxecm-3.0.1.dist-info → pyxecm-3.1.1.dist-info}/WHEEL +0 -0
  52. {pyxecm-3.0.1.dist-info → pyxecm-3.1.1.dist-info}/entry_points.txt +0 -0
@@ -90,14 +90,20 @@ except ModuleNotFoundError:
90
90
  # "wait until" strategy.
91
91
  DEFAULT_WAIT_UNTIL_STRATEGY = "networkidle"
92
92
 
93
- REQUEST_TIMEOUT = 30
94
- REQUEST_RETRY_DELAY = 2
93
+ REQUEST_TIMEOUT = 30.0
94
+ REQUEST_RETRY_DELAY = 2.0
95
95
  REQUEST_MAX_RETRIES = 3
96
96
 
97
97
 
98
98
  class BrowserAutomation:
99
99
  """Class to automate settings via a browser interface."""
100
100
 
101
+ page: Page = None
102
+ browser: Browser = None
103
+ context: BrowserContext = None
104
+ playwright = None
105
+ proxy = None
106
+
101
107
  logger: logging.Logger = default_logger
102
108
 
103
109
  def __init__(
@@ -182,11 +188,10 @@ class BrowserAutomation:
182
188
  self.screenshot_names,
183
189
  "screenshots",
184
190
  )
185
- self.logger.debug("Creating Screenshot directory... -> %s", self.screenshot_directory)
191
+ self.logger.debug("Creating screenshot directory... -> %s", self.screenshot_directory)
186
192
  if self.take_screenshots and not os.path.exists(self.screenshot_directory):
187
193
  os.makedirs(self.screenshot_directory)
188
194
 
189
- self.proxy = None
190
195
  if os.getenv("HTTP_PROXY"):
191
196
  self.proxy = {
192
197
  "server": os.getenv("HTTP_PROXY"),
@@ -194,21 +199,22 @@ class BrowserAutomation:
194
199
  self.logger.info("Using HTTP proxy -> %s", os.getenv("HTTP_PROXY"))
195
200
 
196
201
  browser = browser or os.getenv("BROWSER", "webkit")
197
- self.logger.info("Using Browser -> '%s'...", browser)
202
+ self.logger.info("Using browser -> '%s'...", browser)
198
203
 
199
204
  if not self.setup_playwright(browser=browser):
200
- self.logger.error("Failed to initialize Playwright browser automation!")
201
- return
205
+ msg = "Failed to initialize Playwright browser automation!"
206
+ self.logger.error(msg)
207
+ raise RuntimeError(msg)
202
208
 
203
- self.logger.info("Creating Browser Context...")
209
+ self.logger.info("Creating browser context...")
204
210
  self.context: BrowserContext = self.browser.new_context(
205
211
  accept_downloads=True,
206
212
  )
207
213
 
208
- self.logger.info("Creating Page...")
214
+ self.logger.info("Creating page...")
209
215
  self.page: Page = self.context.new_page()
210
216
  self.main_page = self.page
211
- self.logger.info("Browser Automation initialized.")
217
+ self.logger.info("Browser automation initialized.")
212
218
 
213
219
  # end method definition
214
220
 
@@ -217,7 +223,12 @@ class BrowserAutomation:
217
223
 
218
224
  Args:
219
225
  browser (str):
220
- Name of the browser engine.
226
+ Name of the browser engine. Supported:
227
+ * chromium
228
+ * chrome
229
+ * msedge
230
+ * webkit
231
+ * firefox
221
232
 
222
233
  Returns:
223
234
  bool:
@@ -232,6 +243,9 @@ class BrowserAutomation:
232
243
  self.logger.error("Failed to start Playwright!")
233
244
  return False
234
245
 
246
+ result = True
247
+
248
+ # Install and launch the selected browser in Playwright:
235
249
  match browser:
236
250
  case "chromium":
237
251
  try:
@@ -239,10 +253,11 @@ class BrowserAutomation:
239
253
  headless=self.headless, slow_mo=100 if not self.headless else None, proxy=self.proxy
240
254
  )
241
255
  except Exception:
242
- self.install_browser(browser=browser)
243
- self.browser: Browser = self.playwright.chromium.launch(
244
- headless=self.headless, slow_mo=100 if not self.headless else None, proxy=self.proxy
245
- )
256
+ result = self.install_browser(browser=browser)
257
+ if result:
258
+ self.browser: Browser = self.playwright.chromium.launch(
259
+ headless=self.headless, slow_mo=100 if not self.headless else None, proxy=self.proxy
260
+ )
246
261
 
247
262
  case "chrome":
248
263
  try:
@@ -253,13 +268,14 @@ class BrowserAutomation:
253
268
  proxy=self.proxy,
254
269
  )
255
270
  except Exception:
256
- self.install_browser(browser=browser)
257
- self.browser: Browser = self.playwright.chromium.launch(
258
- channel="chrome",
259
- headless=self.headless,
260
- slow_mo=100 if not self.headless else None,
261
- proxy=self.proxy,
262
- )
271
+ result = self.install_browser(browser=browser)
272
+ if result:
273
+ self.browser: Browser = self.playwright.chromium.launch(
274
+ channel="chrome",
275
+ headless=self.headless,
276
+ slow_mo=100 if not self.headless else None,
277
+ proxy=self.proxy,
278
+ )
263
279
 
264
280
  case "msedge":
265
281
  try:
@@ -270,13 +286,14 @@ class BrowserAutomation:
270
286
  proxy=self.proxy,
271
287
  )
272
288
  except Exception:
273
- self.install_browser(browser=browser)
274
- self.browser: Browser = self.playwright.chromium.launch(
275
- channel="msedge",
276
- headless=self.headless,
277
- slow_mo=100 if not self.headless else None,
278
- proxy=self.proxy,
279
- )
289
+ result = self.install_browser(browser=browser)
290
+ if result:
291
+ self.browser: Browser = self.playwright.chromium.launch(
292
+ channel="msedge",
293
+ headless=self.headless,
294
+ slow_mo=100 if not self.headless else None,
295
+ proxy=self.proxy,
296
+ )
280
297
 
281
298
  case "webkit":
282
299
  try:
@@ -284,10 +301,11 @@ class BrowserAutomation:
284
301
  headless=self.headless, slow_mo=100 if not self.headless else None, proxy=self.proxy
285
302
  )
286
303
  except Exception:
287
- self.install_browser(browser=browser)
288
- self.browser: Browser = self.playwright.webkit.launch(
289
- headless=self.headless, slow_mo=100 if not self.headless else None, proxy=self.proxy
290
- )
304
+ result = self.install_browser(browser=browser)
305
+ if result:
306
+ self.browser: Browser = self.playwright.webkit.launch(
307
+ headless=self.headless, slow_mo=100 if not self.headless else None, proxy=self.proxy
308
+ )
291
309
 
292
310
  case "firefox":
293
311
  try:
@@ -295,16 +313,30 @@ class BrowserAutomation:
295
313
  headless=self.headless, slow_mo=100 if not self.headless else None, proxy=self.proxy
296
314
  )
297
315
  except Exception:
298
- self.install_browser(browser=browser)
299
- self.browser: Browser = self.playwright.firefox.launch(
300
- headless=self.headless, slow_mo=100 if not self.headless else None, proxy=self.proxy
301
- )
302
- return True
316
+ result = self.install_browser(browser=browser)
317
+ if result:
318
+ self.browser: Browser = self.playwright.firefox.launch(
319
+ headless=self.headless, slow_mo=100 if not self.headless else None, proxy=self.proxy
320
+ )
321
+ case _:
322
+ self.logger.error("Unknown browser -> '%s'. Cannot install and launch it.", browser)
323
+ result = False
324
+
325
+ return result
303
326
 
304
327
  # end method definition
305
328
 
306
329
  def install_browser(self, browser: str) -> bool:
307
- """Check if browser is already installed if not install it."""
330
+ """Install a browser with a provided name in Playwright.
331
+
332
+ Args:
333
+ browser (str):
334
+ Name of the browser to be installed.
335
+
336
+ Returns:
337
+ bool: True = installation successful, False = installation failed.
338
+
339
+ """
308
340
 
309
341
  self.logger.info("Installing Browser -> '%s'...", browser)
310
342
  process = subprocess.Popen(
@@ -314,12 +346,13 @@ class BrowserAutomation:
314
346
  shell=False,
315
347
  )
316
348
  output, error = process.communicate()
317
- if process.returncode == 0:
318
- self.logger.info("Installation completed successfullly.")
349
+ if process.returncode == 0: # 0 = success
350
+ self.logger.info("Successfuly completed installation of browser -> '%s'.", browser)
319
351
  self.logger.debug(output.decode())
320
352
  else:
321
- self.logger.error("Installation failed with -> %s", error.decode())
353
+ self.logger.error("Installation of browser -> '%s' failed! Error -> %s", browser, error.decode())
322
354
  self.logger.error(output.decode())
355
+ return False
323
356
 
324
357
  return True
325
358
 
@@ -351,19 +384,21 @@ class BrowserAutomation:
351
384
 
352
385
  # end method definition
353
386
 
354
- def take_screenshot(self) -> bool:
387
+ def take_screenshot(self, suffix: str = "") -> bool:
355
388
  """Take a screenshot of the current browser window and save it as PNG file.
356
389
 
390
+ Args:
391
+ suffix (str, optional):
392
+ Optional suffix to append to the screenshot filename.
393
+
357
394
  Returns:
358
395
  bool:
359
396
  True if successful, False otherwise
360
397
 
361
398
  """
362
399
 
363
- screenshot_file = "{}/{}-{:02d}.png".format(
364
- self.screenshot_directory,
365
- self.screenshot_names,
366
- self.screenshot_counter,
400
+ screenshot_file = "{}/{}-{:02d}{}.png".format(
401
+ self.screenshot_directory, self.screenshot_names, self.screenshot_counter, suffix
367
402
  )
368
403
  self.logger.debug("Save browser screenshot to -> %s", screenshot_file)
369
404
 
@@ -634,6 +669,7 @@ class BrowserAutomation:
634
669
  wait_state: str = "visible",
635
670
  exact_match: bool | None = None,
636
671
  regex: bool = False,
672
+ occurrence: int = 1,
637
673
  iframe: str | None = None,
638
674
  repeat_reload: int | None = None,
639
675
  repeat_reload_delay: int = 60,
@@ -653,10 +689,19 @@ class BrowserAutomation:
653
689
  wait_state (str, optional):
654
690
  Defines if we wait for attached (element is part of DOM) or
655
691
  if we wait for elem to be visible (attached, displayed, and has non-zero size).
692
+ Possible values are:
693
+ * "attached" - the element is present in the DOM.
694
+ * "detached" - the element is not present in the DOM.
695
+ * "visible" - the element is visible (attached, displayed, and has non-zero size).
696
+ * "hidden" - the element is hidden (attached, but not displayed).
697
+ Default is "visible".
656
698
  exact_match (bool | None, optional):
657
699
  If an exact matching is required. Default is None (not set).
658
700
  regex (bool, optional):
659
701
  Should the name be interpreted as a regular expression?
702
+ occurrence (int, optional):
703
+ If multiple elements match the selector, this defines which one to return.
704
+ Default is 1 (the first one).
660
705
  iframe (str | None):
661
706
  Is the element in an iFrame? Then provide the name of the iframe with this parameter.
662
707
  repeat_reload (int | None):
@@ -674,17 +719,22 @@ class BrowserAutomation:
674
719
 
675
720
  """
676
721
 
677
- failure_message = "Cannot find page element with selector -> '{}' ({}){}{}".format(
722
+ failure_message = "Cannot find {} page element with selector -> '{}' ({}){}{}{}{}".format(
723
+ "occurence #{} of".format(occurrence) if occurrence > 1 else "any",
678
724
  selector,
679
725
  selector_type,
680
726
  " and role type -> '{}'".format(role_type) if role_type else "",
681
727
  " in iframe -> '{}'".format(iframe) if iframe else "",
728
+ ", occurrence -> {}".format(occurrence) if occurrence > 1 else "",
729
+ ", waiting for state -> '{}'".format(wait_state),
682
730
  )
683
- success_message = "Found page element with selector -> '{}' ('{}'){}{}".format(
731
+ success_message = "Found {} page element with selector -> '{}' ('{}'){}{}{}".format(
732
+ "occurence #{} of".format(occurrence) if occurrence > 1 else "a",
684
733
  selector,
685
734
  selector_type,
686
735
  " and role type -> '{}'".format(role_type) if role_type else "",
687
736
  " in iframe -> '{}'".format(iframe) if iframe else "",
737
+ ", occurrence -> {}".format(occurrence) if occurrence > 1 else "",
688
738
  )
689
739
 
690
740
  def do_find() -> Locator | None:
@@ -709,8 +759,13 @@ class BrowserAutomation:
709
759
  # are not yet loaded:
710
760
 
711
761
  try:
762
+ index = occurrence - 1 # convert to 0-based index
763
+ if index < 0: # basic validation
764
+ self.logger.error("Occurrence must be >= 1")
765
+ return None
712
766
  self.logger.debug(
713
- "Wait for locator to find element with selector -> '%s' (%s%s%s) and state -> '%s'%s...",
767
+ "Wait for locator to find %selement with selector -> '%s' (%s%s%s) and state -> '%s'%s...",
768
+ "occurrence #{} of ".format(occurrence) if occurrence > 1 else "",
714
769
  selector,
715
770
  "selector type -> '{}'".format(selector_type),
716
771
  ", role type -> '{}'".format(role_type) if role_type else "",
@@ -718,7 +773,9 @@ class BrowserAutomation:
718
773
  wait_state,
719
774
  " in iframe -> '{}'".format(iframe) if iframe else "",
720
775
  )
721
- locator = locator.first
776
+
777
+ locator = locator.first if occurrence == 1 else locator.nth(index)
778
+ # Wait for the element to be in the desired state:
722
779
  locator.wait_for(state=wait_state)
723
780
  except PlaywrightError as pe:
724
781
  if show_error and repeat_reload is None:
@@ -763,6 +820,7 @@ class BrowserAutomation:
763
820
  selector: str,
764
821
  selector_type: str = "id",
765
822
  role_type: str | None = None,
823
+ occurrence: int = 1,
766
824
  scroll_to_element: bool = True,
767
825
  desired_checkbox_state: bool | None = None,
768
826
  is_navigation_trigger: bool = False,
@@ -770,6 +828,7 @@ class BrowserAutomation:
770
828
  is_page_close_trigger: bool = False,
771
829
  wait_until: str | None = None,
772
830
  wait_time: float = 0.0,
831
+ wait_state: str = "visible",
773
832
  exact_match: bool | None = None,
774
833
  regex: bool = False,
775
834
  hover_only: bool = False,
@@ -793,6 +852,9 @@ class BrowserAutomation:
793
852
  role_type (str | None, optional):
794
853
  ARIA role when using selector_type="role", e.g., "button", "textbox".
795
854
  If irrelevant then None should be passed for role_type.
855
+ occurrence (int, optional):
856
+ If multiple elements match the selector, this defines which one to return.
857
+ Default is 1 (the first one).
796
858
  scroll_to_element (bool, optional):
797
859
  Scroll the element into view.
798
860
  desired_checkbox_state (bool | None, optional):
@@ -814,8 +876,17 @@ class BrowserAutomation:
814
876
  This seems to be the safest one for OpenText Content Server.
815
877
  * "domcontentloaded" - waits for the DOMContentLoaded event (HTML is parsed,
816
878
  but subresources may still load).
817
- wait_time (float):
818
- Time in seconds to wait for elements to appear.
879
+ wait_time (float, optional):
880
+ Time in seconds to wait for elements to appear. Default is 0.0 (no wait).
881
+ wait_state (str, optional):
882
+ Defines if we wait for attached (element is part of DOM) or
883
+ if we wait for elem to be visible (attached, displayed, and has non-zero size).
884
+ Possible values are:
885
+ * "attached" - the element is present in the DOM.
886
+ * "detached" - the element is not present in the DOM.
887
+ * "visible" - the element is visible (attached, displayed, and has non-zero size).
888
+ * "hidden" - the element is hidden (attached, but not displayed).
889
+ Default is "visible".
819
890
  exact_match (bool | None, optional):
820
891
  If an exact matching is required. Default is None (not set).
821
892
  regex (bool, optional):
@@ -852,6 +923,14 @@ class BrowserAutomation:
852
923
 
853
924
  """
854
925
 
926
+ if not selector:
927
+ failure_message = "Missing element selector! Cannot find page element!"
928
+ if show_error:
929
+ self.logger.error(failure_message)
930
+ else:
931
+ self.logger.warning(failure_message)
932
+ return False
933
+
855
934
  success = True # Final return value
856
935
 
857
936
  # If no specific wait until strategy is provided in the
@@ -865,20 +944,17 @@ class BrowserAutomation:
865
944
  self.logger.info("Wait for %d milliseconds before clicking...", wait_time * 1000)
866
945
  self.page.wait_for_timeout(wait_time * 1000)
867
946
 
868
- if not selector:
869
- failure_message = "Missing element selector! Cannot find page element!"
870
- if show_error:
871
- self.logger.error(failure_message)
872
- else:
873
- self.logger.warning(failure_message)
874
- return False
947
+ if self.take_screenshots:
948
+ self.take_screenshot(suffix="_wait_before_click")
875
949
 
876
950
  elem = self.find_elem(
877
951
  selector=selector,
878
952
  selector_type=selector_type,
879
953
  role_type=role_type,
954
+ wait_state=wait_state,
880
955
  exact_match=exact_match,
881
956
  regex=regex,
957
+ occurrence=occurrence,
882
958
  iframe=iframe,
883
959
  repeat_reload=repeat_reload,
884
960
  repeat_reload_delay=repeat_reload_delay,
@@ -977,6 +1053,7 @@ class BrowserAutomation:
977
1053
  value: str | bool,
978
1054
  selector_type: str = "id",
979
1055
  role_type: str | None = None,
1056
+ occurrence: int = 1,
980
1057
  is_sensitive: bool = False,
981
1058
  press_enter: bool = False,
982
1059
  exact_match: bool | None = None,
@@ -998,6 +1075,9 @@ class BrowserAutomation:
998
1075
  role_type (str | None, optional):
999
1076
  ARIA role when using selector_type="role", e.g., "button", "textbox".
1000
1077
  If irrelevant then None should be passed for role_type.
1078
+ occurrence (int, optional):
1079
+ If multiple elements match the selector, this defines which one to return.
1080
+ Default is 1 (the first one).
1001
1081
  is_sensitive (bool, optional):
1002
1082
  True for suppressing sensitive information in logging.
1003
1083
  press_enter (bool, optional):
@@ -1028,6 +1108,7 @@ class BrowserAutomation:
1028
1108
  role_type=role_type,
1029
1109
  exact_match=exact_match,
1030
1110
  regex=regex,
1111
+ occurrence=occurrence,
1031
1112
  iframe=iframe,
1032
1113
  show_error=True,
1033
1114
  )
@@ -1172,7 +1253,7 @@ class BrowserAutomation:
1172
1253
  self.logger.error("Download failed; error -> %s", str(e))
1173
1254
  return None
1174
1255
 
1175
- self.logger.info("Download file to -> %s", save_path)
1256
+ self.logger.info("Downloaded file to -> %s", save_path)
1176
1257
 
1177
1258
  return save_path
1178
1259
 
@@ -1223,13 +1304,13 @@ class BrowserAutomation:
1223
1304
  Is the element in an iFrame? Then provide the name of the iframe with this parameter.
1224
1305
  min_count (int):
1225
1306
  Minimum number of required matches (# elements on page).
1226
- wait_time (float):
1227
- Time in seconds to wait for elements to appear.
1307
+ wait_time (float, optional):
1308
+ Time in seconds to wait for elements to appear. Default is 0.0 (no wait).
1228
1309
  wait_state (str, optional):
1229
1310
  Defines if we wait for attached (element is part of DOM) or
1230
1311
  if we wait for elem to be visible (attached, displayed, and has non-zero size).
1231
- show_error (bool):
1232
- Whether to log warnings/errors.
1312
+ show_error (bool, optional):
1313
+ Whether to log warnings/errors. Default is True.
1233
1314
 
1234
1315
  Returns:
1235
1316
  bool | None:
@@ -1256,14 +1337,13 @@ class BrowserAutomation:
1256
1337
  iframe=iframe,
1257
1338
  )
1258
1339
  if not locator:
1259
- if show_error:
1260
- self.logger.error(
1261
- "Failed to check if elements -> '%s' (%s) exist! Locator is undefined.", selector, selector_type
1262
- )
1340
+ self.logger.error(
1341
+ "Failed to check if elements -> '%s' (%s) exist! Locator is undefined.", selector, selector_type
1342
+ )
1263
1343
  return (None, 0)
1264
1344
 
1265
1345
  self.logger.info(
1266
- "Check if at least %d element%s found by selector -> %s (%s%s)%s%s%s...",
1346
+ "Check if at least %d element%s found by selector -> '%s' (%s%s)%s%s%s...",
1267
1347
  min_count,
1268
1348
  "s are" if min_count > 1 else " is",
1269
1349
  selector,
@@ -1319,8 +1399,9 @@ class BrowserAutomation:
1319
1399
  return (None, 0)
1320
1400
 
1321
1401
  self.logger.info(
1322
- "Found %s elements matching selector -> '%s' (%s%s).",
1402
+ "Found %d element%s matching selector -> '%s' (%s%s).",
1323
1403
  count,
1404
+ "s" if count > 1 else "",
1324
1405
  selector,
1325
1406
  "selector type -> '{}'".format(selector_type),
1326
1407
  ", role type -> '{}'".format(role_type) if role_type else "",
@@ -1336,7 +1417,7 @@ class BrowserAutomation:
1336
1417
 
1337
1418
  matching_elems = []
1338
1419
 
1339
- # Iterate over all elements found by the locator and checkif
1420
+ # Iterate over all elements found by the locator and check if
1340
1421
  # they comply with the additional value conditions (if provided).
1341
1422
  # We collect all matching elements in a list:
1342
1423
  for i in range(count):
@@ -1374,8 +1455,9 @@ class BrowserAutomation:
1374
1455
  else:
1375
1456
  success = True
1376
1457
  self.logger.info(
1377
- "Found %d matching elements.%s",
1458
+ "Found %d matching element%s.%s",
1378
1459
  matching_elements_count,
1460
+ "s" if matching_elements_count > 1 else "",
1379
1461
  " This is {} the minimum {} element{} probed for.".format(
1380
1462
  "exactly" if matching_elements_count == min_count else "more than",
1381
1463
  min_count,
@@ -1467,7 +1549,7 @@ class BrowserAutomation:
1467
1549
  return False
1468
1550
 
1469
1551
  if "Verify" in title:
1470
- self.logger.error("Site is asking for a Verification Token. You may need to whitelist your IP!")
1552
+ self.logger.error("Site is asking for a verification token. You may need to whitelist your IP!")
1471
1553
  return False
1472
1554
  if "Login" in title:
1473
1555
  self.logger.error("Authentication failed. You may have given the wrong password!")
@@ -1492,9 +1574,9 @@ class BrowserAutomation:
1492
1574
 
1493
1575
  """
1494
1576
 
1495
- self.logger.debug("Setting default timeout to -> %s seconds...", str(wait_time))
1577
+ self.logger.debug("Setting default timeout to -> %.2f seconds...", wait_time)
1496
1578
  self.page.set_default_timeout(wait_time * 1000)
1497
- self.logger.debug("Setting navigation timeout to -> %s seconds...", str(wait_time))
1579
+ self.logger.debug("Setting navigation timeout to -> %.2f seconds...", wait_time)
1498
1580
  self.page.set_default_navigation_timeout(wait_time * 1000)
1499
1581
 
1500
1582
  # end method definition
@@ -1502,11 +1584,11 @@ class BrowserAutomation:
1502
1584
  def end_session(self) -> None:
1503
1585
  """End the browser session and close the browser."""
1504
1586
 
1505
- self.logger.info("Close Browser Page...")
1587
+ self.logger.info("Close browser page...")
1506
1588
  self.page.close()
1507
- self.logger.info("Close Browser Context...")
1589
+ self.logger.info("Close browser context...")
1508
1590
  self.context.close()
1509
- self.logger.info("Close Browser...")
1591
+ self.logger.info("Close browser...")
1510
1592
  self.browser.close()
1511
1593
  self.logged_in = False
1512
1594
  self.logger.info("Stop Playwright instance...")