pyxecm 1.5__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.
- pyxecm/__init__.py +6 -2
- pyxecm/avts.py +1492 -0
- pyxecm/coreshare.py +1075 -960
- pyxecm/customizer/__init__.py +16 -4
- pyxecm/customizer/__main__.py +58 -0
- pyxecm/customizer/api/__init__.py +5 -0
- pyxecm/customizer/api/__main__.py +6 -0
- pyxecm/customizer/api/app.py +914 -0
- pyxecm/customizer/api/auth.py +154 -0
- pyxecm/customizer/api/metrics.py +92 -0
- pyxecm/customizer/api/models.py +13 -0
- pyxecm/customizer/api/payload_list.py +865 -0
- pyxecm/customizer/api/settings.py +103 -0
- pyxecm/customizer/browser_automation.py +332 -139
- pyxecm/customizer/customizer.py +1075 -1057
- pyxecm/customizer/exceptions.py +35 -0
- pyxecm/customizer/guidewire.py +322 -0
- pyxecm/customizer/k8s.py +787 -338
- pyxecm/customizer/log.py +107 -0
- pyxecm/customizer/m365.py +3424 -2270
- pyxecm/customizer/nhc.py +1169 -0
- pyxecm/customizer/openapi.py +258 -0
- pyxecm/customizer/payload.py +18201 -7030
- pyxecm/customizer/pht.py +1047 -210
- pyxecm/customizer/salesforce.py +836 -727
- pyxecm/customizer/sap.py +58 -41
- pyxecm/customizer/servicenow.py +851 -383
- pyxecm/customizer/settings.py +442 -0
- pyxecm/customizer/successfactors.py +408 -346
- pyxecm/customizer/translate.py +83 -48
- pyxecm/helper/__init__.py +5 -2
- pyxecm/helper/assoc.py +98 -38
- pyxecm/helper/data.py +2482 -742
- pyxecm/helper/logadapter.py +27 -0
- pyxecm/helper/web.py +229 -101
- pyxecm/helper/xml.py +528 -172
- pyxecm/maintenance_page/__init__.py +5 -0
- pyxecm/maintenance_page/__main__.py +6 -0
- pyxecm/maintenance_page/app.py +51 -0
- pyxecm/maintenance_page/settings.py +28 -0
- pyxecm/maintenance_page/static/favicon.avif +0 -0
- pyxecm/maintenance_page/templates/maintenance.html +165 -0
- pyxecm/otac.py +234 -140
- pyxecm/otawp.py +2689 -0
- pyxecm/otcs.py +12344 -7547
- pyxecm/otds.py +3166 -2219
- pyxecm/otiv.py +36 -21
- pyxecm/otmm.py +1363 -296
- pyxecm/otpd.py +231 -127
- pyxecm-2.0.0.dist-info/METADATA +145 -0
- pyxecm-2.0.0.dist-info/RECORD +54 -0
- {pyxecm-1.5.dist-info → pyxecm-2.0.0.dist-info}/WHEEL +1 -1
- pyxecm-1.5.dist-info/METADATA +0 -51
- pyxecm-1.5.dist-info/RECORD +0 -30
- {pyxecm-1.5.dist-info → pyxecm-2.0.0.dist-info/licenses}/LICENSE +0 -0
- {pyxecm-1.5.dist-info → pyxecm-2.0.0.dist-info}/top_level.txt +0 -0
|
@@ -1,81 +1,118 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
47
|
-
|
|
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
|
|
64
|
-
|
|
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 =
|
|
74
|
+
download_directory: str | None = None,
|
|
76
75
|
take_screenshots: bool = False,
|
|
77
|
-
automation_name: str = "
|
|
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 =
|
|
90
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
self.browser
|
|
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
|
-
"""
|
|
108
|
-
|
|
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",
|
|
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:
|
|
183
|
+
bool:
|
|
184
|
+
True if successful, False otherwise
|
|
185
|
+
|
|
135
186
|
"""
|
|
136
187
|
|
|
137
188
|
screenshot_file = "{}/{}-{}.png".format(
|
|
138
|
-
self.screenshot_directory,
|
|
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):
|
|
203
|
+
url (str):
|
|
204
|
+
URL to load. If empty just the base URL will be used
|
|
151
205
|
Returns:
|
|
152
|
-
bool:
|
|
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
|
-
|
|
161
|
-
|
|
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.
|
|
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:
|
|
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):
|
|
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(
|
|
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
|
|
211
|
-
logger.error("WebDriverException occurred
|
|
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):
|
|
225
|
-
|
|
226
|
-
|
|
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:
|
|
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
|
|
324
|
+
except NoSuchElementException:
|
|
248
325
|
if show_error:
|
|
249
|
-
logger.error(
|
|
250
|
-
"Cannot find page element -> %s by -> %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
|
|
264
|
-
logger.error(
|
|
265
|
-
"Timed out waiting for the element to be present or visible
|
|
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
|
|
270
|
-
logger.error("Element is not interactable
|
|
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
|
|
276
|
-
logger.error("WebDriverException occurred
|
|
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):
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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:
|
|
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,
|
|
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
|
-
|
|
444
|
+
StaleElementReferenceException,
|
|
445
|
+
InvalidElementStateException,
|
|
446
|
+
):
|
|
326
447
|
if show_error:
|
|
327
|
-
logger.error(
|
|
328
|
-
"Cannot click page element -> %s
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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
|
-
|
|
376
|
-
elem.
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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,
|
|
557
|
+
self,
|
|
558
|
+
find_elem: str,
|
|
559
|
+
find_method: str = By.ID,
|
|
560
|
+
download_time: int = 30,
|
|
392
561
|
) -> str | None:
|
|
393
|
-
"""
|
|
562
|
+
"""Click a page element to initiate a download.
|
|
394
563
|
|
|
395
564
|
Args:
|
|
396
|
-
find_elem (str):
|
|
397
|
-
|
|
398
|
-
|
|
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:
|
|
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,
|
|
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
|
-
"""
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
492
|
-
|
|
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
|