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.
- pyxecm/__init__.py +3 -0
- pyxecm/coreshare.py +2636 -0
- pyxecm/customizer/__init__.py +6 -0
- pyxecm/customizer/browser_automation.py +231 -56
- pyxecm/customizer/customizer.py +466 -235
- pyxecm/customizer/k8s.py +49 -27
- pyxecm/customizer/m365.py +1183 -263
- pyxecm/customizer/payload.py +13854 -5368
- pyxecm/customizer/pht.py +503 -0
- pyxecm/customizer/salesforce.py +1782 -0
- pyxecm/customizer/sap.py +5 -5
- pyxecm/customizer/servicenow.py +1221 -0
- pyxecm/customizer/successfactors.py +1056 -0
- pyxecm/customizer/translate.py +2 -2
- pyxecm/helper/__init__.py +2 -0
- pyxecm/helper/assoc.py +27 -7
- pyxecm/helper/data.py +1527 -0
- pyxecm/helper/web.py +189 -25
- pyxecm/helper/xml.py +244 -40
- pyxecm/otac.py +311 -25
- pyxecm/otcs.py +3866 -1103
- pyxecm/otds.py +397 -150
- pyxecm/otiv.py +1 -1
- pyxecm/otmm.py +808 -0
- pyxecm/otpd.py +17 -12
- {pyxecm-1.3.0.dist-info → pyxecm-1.5.dist-info}/METADATA +4 -1
- pyxecm-1.5.dist-info/RECORD +30 -0
- {pyxecm-1.3.0.dist-info → pyxecm-1.5.dist-info}/WHEEL +1 -1
- pyxecm-1.3.0.dist-info/RECORD +0 -23
- {pyxecm-1.3.0.dist-info → pyxecm-1.5.dist-info}/LICENSE +0 -0
- {pyxecm-1.3.0.dist-info → pyxecm-1.5.dist-info}/top_level.txt +0 -0
pyxecm/customizer/__init__.py
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
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.
|
|
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.
|
|
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
|
|
129
|
-
"""
|
|
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
|
-
|
|
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
|
|
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
|
-
"
|
|
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
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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,
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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(
|
|
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.
|
|
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
|
|
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.
|
|
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):
|