pyxecm 1.3.0__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.

@@ -1,6 +1,12 @@
1
1
  """PYXECM classes for Customizer"""
2
+
3
+ from .browser_automation import BrowserAutomation
4
+
2
5
  from .customizer import Customizer
3
6
  from .k8s import K8s
4
7
  from .m365 import M365
5
8
  from .payload import Payload
6
9
  from .sap import SAP
10
+ from .salesforce import Salesforce
11
+ from .successfactors import SuccessFactors
12
+ from .servicenow import ServiceNow
@@ -6,10 +6,17 @@ no REST API or LLConfig can be used.
6
6
  Class: BrowserAutomation
7
7
  Methods:
8
8
 
9
- __init__ : class initializer
9
+ __init__ : class initializer. Start the browser session.
10
10
  set_chrome_options: Sets chrome options for Selenium. Chrome options for headless browser is enabled
11
- run_otcs_login: Login to OTCS via the browser
12
- run_configure_vertex_datasource: Run the configuration of the Aviator Vertex datasource
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/
13
20
  end_session: End the browser session
14
21
  """
15
22
 
@@ -25,12 +32,17 @@ try:
25
32
  from selenium.webdriver.chrome.options import Options
26
33
  from selenium import webdriver
27
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
28
37
  from selenium.common.exceptions import (
29
38
  WebDriverException,
30
39
  NoSuchElementException,
31
40
  ElementNotInteractableException,
32
41
  ElementClickInterceptedException,
42
+ TimeoutException,
43
+ MoveTargetOutOfBoundsException,
33
44
  )
45
+
34
46
  except ModuleNotFoundError as module_exception:
35
47
  logger.warning("Module selenium is not installed")
36
48
 
@@ -42,11 +54,14 @@ except ModuleNotFoundError as module_exception:
42
54
 
43
55
  ID: str = ""
44
56
 
57
+ class WebElement:
58
+ """Dummy class to avoid errors if selenium module cannot be imported"""
59
+
45
60
 
46
61
  try:
47
62
  import chromedriver_autoinstaller
48
63
  except ModuleNotFoundError as module_exception:
49
- logger.warning("Module chromedriver_autoinstaller is not installed")
64
+ logger.warning("Module chromedriver_autoinstaller is not installed!")
50
65
 
51
66
 
52
67
  class BrowserAutomation:
@@ -54,10 +69,12 @@ class BrowserAutomation:
54
69
 
55
70
  def __init__(
56
71
  self,
57
- base_url: str,
58
- user_name: str,
59
- user_password: str,
72
+ base_url: str = "",
73
+ user_name: str = "",
74
+ user_password: str = "",
60
75
  download_directory: str = "/tmp",
76
+ take_screenshots: bool = False,
77
+ automation_name: str = "screen",
61
78
  ) -> None:
62
79
  self.base_url = base_url
63
80
  self.user_name = user_name
@@ -65,6 +82,16 @@ class BrowserAutomation:
65
82
  self.logged_in = False
66
83
  self.download_directory = download_directory
67
84
 
85
+ self.take_screenshots = take_screenshots
86
+ self.screenshot_names = automation_name
87
+ self.screen_counter = 1
88
+
89
+ self.screenshot_directory = "/tmp/browser_automations/{}".format(
90
+ automation_name
91
+ )
92
+
93
+ if self.take_screenshots and not os.path.exists(self.screenshot_directory):
94
+ os.makedirs(self.screenshot_directory)
68
95
  chromedriver_autoinstaller.install()
69
96
  self.browser = webdriver.Chrome(options=self.set_chrome_options())
70
97
 
@@ -100,9 +127,24 @@ class BrowserAutomation:
100
127
 
101
128
  # end method definition
102
129
 
130
+ def take_screenshot(self) -> bool:
131
+ """Take a screenshot of the current browser window and save it as PNG file
132
+
133
+ Returns:
134
+ bool: True if successful, False otherwise
135
+ """
136
+
137
+ screenshot_file = "{}/{}-{}.png".format(
138
+ self.screenshot_directory, self.screenshot_names, self.screen_counter
139
+ )
140
+ logger.debug("Save browser screenshot to -> %s", screenshot_file)
141
+ result = self.browser.get_screenshot_as_file(screenshot_file)
142
+ self.screen_counter += 1
143
+
144
+ return result
145
+
103
146
  def get_page(self, url: str = "") -> bool:
104
147
  """Load a page into the browser based on a given URL.
105
- Required authorization need
106
148
 
107
149
  Args:
108
150
  url (str): URL to load. If empty just the base URL will be used
@@ -113,26 +155,77 @@ class BrowserAutomation:
113
155
  page_url = self.base_url + url
114
156
 
115
157
  try:
116
- logger.info("Load page -> %s", page_url)
158
+ logger.debug("Load page -> %s", page_url)
117
159
  self.browser.get(page_url)
118
160
  except WebDriverException as exception:
119
161
  logger.error("Cannot load page -> %s; error -> %s", page_url, exception)
120
162
  return False
121
163
 
122
- logger.info("Page title after get page -> %s", self.browser.title)
164
+ logger.debug("Page title after get page -> %s", self.browser.title)
165
+
166
+ if self.take_screenshots:
167
+ self.take_screenshot()
123
168
 
124
169
  return True
125
170
 
126
171
  # end method definition
127
172
 
128
- def find_elem_and_click(self, find_elem: str, find_method: str = By.ID) -> bool:
129
- """Find an page element and click it.
173
+ def get_title(self) -> str:
174
+ """Get the browser title. This is handy to validate a certain page is loaded after get_page()
175
+
176
+ Returns:
177
+ str: Title of the browser window
178
+ """
179
+
180
+ if not self.browser:
181
+ logger.error("Browser not initialized!")
182
+ return None
183
+
184
+ return self.browser.title
185
+
186
+ # end method definition
187
+
188
+ def scroll_to_element(self, element: WebElement):
189
+ """Scroll an element into view to make it clickable
190
+
191
+ Args:
192
+ element (WebElement): Web element that has been identified before
193
+ """
194
+
195
+ if not element:
196
+ logger.error("Undefined element!")
197
+ return
198
+
199
+ try:
200
+ actions = ActionChains(self.browser)
201
+ actions.move_to_element(element).perform()
202
+ except NoSuchElementException:
203
+ logger.error("Element not found in the DOM")
204
+ except TimeoutException:
205
+ logger.error("Timed out waiting for the element to be present or visible")
206
+ except ElementNotInteractableException:
207
+ logger.error("Element is not interactable!")
208
+ except MoveTargetOutOfBoundsException:
209
+ logger.error("Element is out of bounds!")
210
+ except WebDriverException as e:
211
+ logger.error("WebDriverException occurred -> %s", str(e))
212
+
213
+ # end method definition
214
+
215
+ def find_elem(
216
+ self,
217
+ find_elem: str,
218
+ find_method: str = By.ID,
219
+ show_error: bool = True,
220
+ ) -> WebElement:
221
+ """Find an page element.
130
222
 
131
223
  Args:
132
224
  find_elem (str): name of the page element
133
- find_method (str): either By.ID, By.NAME, By.CLASS_NAME, BY.XPATH
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
134
227
  Returns:
135
- bool: True if successful, False otherwise
228
+ WebElement: web element or None in case an error occured.
136
229
  """
137
230
 
138
231
  # We don't want to expose class "By" outside this module,
@@ -147,72 +240,136 @@ class BrowserAutomation:
147
240
  find_method = By.XPATH
148
241
  else:
149
242
  logger.error("Unsupported find method!")
150
- return False
243
+ return None
151
244
 
152
245
  try:
153
246
  elem = self.browser.find_element(by=find_method, value=find_elem)
154
247
  except NoSuchElementException as exception:
248
+ if show_error:
249
+ logger.error(
250
+ "Cannot find page element -> %s by -> %s; error -> %s",
251
+ find_elem,
252
+ find_method,
253
+ exception,
254
+ )
255
+ return None
256
+ else:
257
+ logger.warning(
258
+ "Cannot find page element -> %s by -> %s",
259
+ find_elem,
260
+ find_method,
261
+ )
262
+ return None
263
+ except TimeoutException as exception:
155
264
  logger.error(
156
- "Cannot find page element -> %s by -> %s; error -> %s",
157
- find_elem,
158
- find_method,
265
+ "Timed out waiting for the element to be present or visible; error -> %s",
159
266
  exception,
160
267
  )
268
+ return None
269
+ except ElementNotInteractableException as exception:
270
+ logger.error("Element is not interactable!; error -> %s", exception)
271
+ return None
272
+ except MoveTargetOutOfBoundsException:
273
+ logger.error("Element is out of bounds!")
274
+ return None
275
+ except WebDriverException as e:
276
+ logger.error("WebDriverException occurred -> %s", str(e))
277
+ return None
278
+
279
+ logger.debug("Found page element -> %s by -> %s", find_elem, find_method)
280
+
281
+ return elem
282
+
283
+ # end method definition
284
+
285
+ def find_elem_and_click(
286
+ self,
287
+ find_elem: str,
288
+ find_method: str = By.ID,
289
+ scroll_to_element: bool = True,
290
+ show_error: bool = True,
291
+ ) -> bool:
292
+ """Find an page element and click it.
293
+
294
+ 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
299
+ Returns:
300
+ bool: True if successful, False otherwise
301
+ """
302
+
303
+ if not find_elem:
304
+ if show_error:
305
+ logger.error("Missing element name! Cannot find HTML element!")
306
+ else:
307
+ logger.warning("Missing element name! Cannot find HTML element!")
161
308
  return False
162
309
 
310
+ elem = self.find_elem(
311
+ find_elem=find_elem, find_method=find_method, show_error=show_error
312
+ )
313
+
314
+ if not elem:
315
+ return not show_error
316
+
163
317
  try:
318
+ if scroll_to_element:
319
+ self.scroll_to_element(elem)
320
+
164
321
  elem.click()
165
- except ElementClickInterceptedException as exception:
166
- logger.error(
167
- "Cannot click page element -> %s; error -> %s", find_elem, exception
168
- )
169
- return False
322
+ except (
323
+ ElementClickInterceptedException,
324
+ ElementNotInteractableException,
325
+ ) as exception:
326
+ if show_error:
327
+ logger.error(
328
+ "Cannot click page element -> %s; error -> %s", find_elem, exception
329
+ )
330
+ return False
331
+ else:
332
+ logger.warning("Cannot click page element -> %s", find_elem)
333
+ return True
334
+
335
+ logger.debug("Successfully clicked element -> %s", find_elem)
336
+
337
+ if self.take_screenshots:
338
+ self.take_screenshot()
170
339
 
171
340
  return True
172
341
 
173
342
  # end method definition
174
343
 
175
344
  def find_elem_and_set(
176
- self, find_elem: str, elem_value: str, find_method: str = By.ID
345
+ self,
346
+ find_elem: str,
347
+ elem_value: str,
348
+ find_method: str = By.ID,
349
+ is_sensitive: bool = False,
177
350
  ) -> bool:
178
351
  """Find an page element and fill it with a new text.
179
352
 
180
353
  Args:
181
354
  find_elem (str): name of the page element
182
355
  elem_value (str): new text string for the page element
183
- find_method (str): either By.ID, By.NAME, By.CLASS_NAME, or By.XPATH
356
+ find_method (str, optional): either By.ID, By.NAME, By.CLASS_NAME, or By.XPATH
357
+ is_sensitive (bool, optional): True for suppressing sensitive information in logging
184
358
  Returns:
185
359
  bool: True if successful, False otherwise
186
360
  """
187
361
 
188
- # We don't want to expose class "By" outside this module,
189
- # so we map the string values to the By class values:
190
- if find_method == "id":
191
- find_method = By.ID
192
- elif find_method == "name":
193
- find_method = By.NAME
194
- elif find_method == "class_name":
195
- find_method = By.CLASS_NAME
196
- elif find_method == "xpath":
197
- find_method = By.XPATH
198
- else:
199
- logger.error("Unsupported find method!")
200
- return False
201
-
202
- logger.info("Try to find element -> %s by -> %s...", find_elem, find_method)
362
+ elem = self.find_elem(
363
+ find_elem=find_elem, find_method=find_method, show_error=True
364
+ )
203
365
 
204
- try:
205
- elem = self.browser.find_element(find_method, find_elem)
206
- except NoSuchElementException as exception:
207
- logger.error(
208
- "Cannot find page element -> %s by -> %s; error -> %s",
209
- find_elem,
210
- find_method,
211
- exception,
212
- )
366
+ if not elem:
213
367
  return False
214
368
 
215
- logger.info("Set element -> %s to value -> %s...", find_elem, elem_value)
369
+ if not is_sensitive:
370
+ logger.debug("Set element -> %s to value -> %s...", find_elem, elem_value)
371
+ else:
372
+ logger.debug("Set element -> %s to value -> <sensitive>...", find_elem)
216
373
 
217
374
  try:
218
375
  elem.clear() # clear existing text in the input field
@@ -272,30 +429,46 @@ class BrowserAutomation:
272
429
  user_field: str = "otds_username",
273
430
  password_field: str = "otds_password",
274
431
  login_button: str = "loginbutton",
432
+ page: str = "",
275
433
  ) -> bool:
276
434
  """Login to target system via the browser"""
277
435
 
278
436
  self.logged_in = False
279
437
 
280
438
  if (
281
- not self.get_page() # assuming the base URL leads towards the login page
439
+ not self.get_page(
440
+ url=page
441
+ ) # assuming the base URL leads towards the login page
282
442
  or not self.find_elem_and_set(
283
443
  find_elem=user_field, elem_value=self.user_name
284
444
  )
285
445
  or not self.find_elem_and_set(
286
- find_elem=password_field, elem_value=self.user_password
446
+ find_elem=password_field,
447
+ elem_value=self.user_password,
448
+ is_sensitive=True,
287
449
  )
288
450
  or not self.find_elem_and_click(find_elem=login_button)
289
451
  ):
290
- logger.error("Cannot log into target system using URL -> %s", self.base_url)
452
+ logger.error(
453
+ "Cannot log into target system using URL -> %s and user -> %s",
454
+ self.base_url,
455
+ self.user_name,
456
+ )
291
457
  return False
292
458
 
293
- logger.info("Page title after login -> %s", self.browser.title)
459
+ logger.debug("Page title after login -> %s", self.browser.title)
460
+
461
+ # Some special handling for Salesforce login:
294
462
  if "Verify" in self.browser.title:
295
463
  logger.error(
296
464
  "Site is asking for a Verification Token. You may need to whitelist your IP!"
297
465
  )
298
466
  return False
467
+ if "Login" in self.browser.title:
468
+ logger.error(
469
+ "Authentication failed. You may have given the wrong password!"
470
+ )
471
+ return False
299
472
 
300
473
  self.logged_in = True
301
474
 
@@ -303,14 +476,16 @@ class BrowserAutomation:
303
476
 
304
477
  # end method definition
305
478
 
306
- def implict_wait(self, wait_time: float):
479
+ def implicit_wait(self, wait_time: float):
307
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.
308
483
 
309
484
  Args:
310
485
  wait_time (float): time in seconds to wait
311
486
  """
312
487
 
313
- logger.info("Implicit wait for max -> %s seconds...", str(wait_time))
488
+ logger.debug("Implicit wait for max -> %s seconds...", str(wait_time))
314
489
  self.browser.implicitly_wait(wait_time)
315
490
 
316
491
  def end_session(self):