def accept(self, key: str = None, timeout: int = 3600, **opt) -> Union[tpsup.nettools.encryptedsocket, None]: verbose = opt.get('verbose', 0) if verbose: tplog( f"waiting for new client connection. time out after {timeout} idle seconds" ) selector = _ServerSelector() selector.register(self.socket, selectors.EVENT_READ) poll_interval = 1 waited_so_far = 0 while waited_so_far < timeout: if verbose > 2: print("looping") ready = selector.select(poll_interval) if ready: (clientsocket, address) = self.socket.accept() if verbose: tplog( f"accepted client socket {clientsocket}, address={address}" ) return tpsup.nettools.encryptedsocket( established_socket=clientsocket, key=key) else: waited_so_far += poll_interval return None
def timeout_child(conn: multiprocessing.connection.Connection, func: types.FunctionType, *args, **kwargs): """ wrapper function for timeout_func :param conn: :param func: :param args: :param kwargs: :return: """ verbose = kwargs.get('verbose', 0) if verbose: tplog(f"func={func}") if not func: # https://stackoverflow.com/questions/43369648/cant-get-attribute-function-inner-on-module-mp-main-from-e-python # when the multiprocessing library copies your main module, it won't run it as the __main__ script and # therefore anything defined inside the if __name__ == '__main__' is not defined in the child process # namespace. Hence, the AttributeError message = f"both func={func} is not initialized. note: func cannot be defined in __main__" tb = pformat( traceback.format_stack()) # outside exception use format_stack() conn.send(RuntimeError(f"{message}\n{tb}")) else: result = None try: result = func(*args, **kwargs) conn.send(result) except Exception as e: tb = pformat( traceback.format_exc()) # within exception use format_exc() conn.send( RuntimeError( f"child process exception, pid={os.getpid()}\n{tb}")) conn.close()
def __init__(self, key: str, established_socket: socket.socket = None, host_port: str = None, maxtry: int = 5, try_interval: int = 3, **opt): if established_socket: self.socket = established_socket elif host_port: host, port = host_port.split(':') if not port: raise RuntimeError( f"bad format at host_port='{host_port}'; expected host:port" ) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) if sock: for i in range(0, maxtry): try: sock.connect((host, int(port))) # tplog(f"connected to {sock}") # __init__ connected to <socket.socket fd=3, family=AddressFamily.AF_INET, # type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 36690), raddr=('127.0.0.1', 29999)> laddr = sock.getsockname() raddr = sock.getpeername() tplog( f"connected: local {laddr[0]}:{laddr[1]}, remote {raddr[0]}:{raddr[1]}" ) tplog( f"connected: local {laddr[0]}:{laddr[1]}, remote {raddr[0]}:{raddr[1]}" ) self.socket = sock break except Exception as e: tplog( f'{i + 1} try out of {maxtry} failed to connect: {e}', file=sys.stderr) if i + 1 < maxtry: tplog(f'will retry after {try_interval}', file=sys.stderr) time.sleep(try_interval) else: raise RuntimeError( f"failed to connect to {host}:{port} in {maxtry} tries" ) else: raise RuntimeError(socket.error) else: raise RuntimeError( "neither established_socket nor host_port specified") if key is None or key == '': key = None self.key = key self.in_coder = tpsup.coder.Coder(key) self.out_coder = tpsup.coder.Coder(key)
def get_attrs(self, element: WebElement, method: str = 'bs4', verbose: int = 0): """ get list of attributes from an element. https://stackoverflow.com/questions/27307131/selenium-webdriver-how-do-i-find-all-of-an-elements-attributes somehow, webdriver doesn't have an API for this :param element: :param method: :return: """ """ note: for html '<div class="login-greeting">Hi LCA Editor Tester,</div>' bs4 will give: {'class': ['login-greeting']} js will give: {'class': 'login-greeting' } """ if method == 'bs4': html: str = element.get_attribute('outerHTML') if verbose: tplog(f"outerHTML={html}") if html: attrs = {} soup = BeautifulSoup(html, 'html.parser') # https://www.crummy.com/software/BeautifulSoup/bs4/doc/#attributes for element in soup(): # soup() is a generator # element.attrs is a dict attrs.update(element.attrs) return attrs else: return {} elif method == 'js': # java script. duplicate attributes will be overwritten js_script = 'var items = {}; ' \ 'for (index = 0; index < arguments[0].attributes.length; ++index) { ' \ ' items[arguments[0].attributes[index].name] = arguments[0].attributes[index].value' \ '};' \ 'return items;' attrs = self.driver.execute_script(js_script, element) return attrs else: raise RuntimeError( f"unsupported method={method}. accepted: bs4 or js")
def main(): print( '------- test timeout_func_unix(). should work on Unix and fail on windows' ) try: sleep_5 = timeout_func_on_unix(2, module_sleep_and_tick, 3) sleep_5(10) except Exception as e: # error on Windows # AttributeError: module 'signal' has no attribute 'SIGALRM' print_exception(e) def main_sleep_and_tick(duration: int, *args, **opt) -> str: """ this is local function: https://stackoverflow.com/questions/36994839/i-can-pickle-local-objects-if-i-use-a-derived-class """ print(f"args = {pformat(args)}") print(f"opt = {pformat(opt)}") for i in range(duration): time.sleep(1) print('tick') message = f"pid={os.getpid()}" print(message) return message b = main_sleep_and_tick print(f"type={type(module_sleep_and_tick)}") print( '------- test timeout_func(). with function defined in module, should work but will time out' ) try: result = timeout_func(2, module_sleep_and_tick, 10, message="should timeout") tplog(f"result={pformat(result)}") except TimeoutException as e: tplog_exception(e) tplog("got expected exception\n\n") print( '------- test timeout_func(). with function defined in main, may work on unix but should fail on windows ' 'with pickle error') try: result = timeout_func(2, main_sleep_and_tick, 10, message="should timeout") tplog(f"result={pformat(result)}") except AttributeError as e: # AttributeError: Can't pickle local object 'main.<locals>.main_sleep_and_tick' tplog_exception(e) tplog("got expected exception\n\n") print('\n------- test timeout_func(). child will raise exception') try: result = timeout_func(3, test_child_exception) tplog(f"result={pformat(result)}") except TimeoutException as e: tplog_exception(e) tplog("got expected exception\n\n")
def timeout_func(timeout: int, func: Callable, *args, **kwargs): """ time out a func. signal.ALRM not working on Windows. therefore this https://stackoverflow.com/questions/492519/timeout-on-a-function-call so we use multiprocessing.Pipe() to do this https://docs.python.org/3.8/library/multiprocessing.html use this method because we can 1. multiprocessing is process-based. process is easier to check and kill after timeout 2. p.join(timeout) is easier to set timeout 3. Pipe() is easier to collect return value there is decorator using a same method. It has covered a lot of edge conidtions. good place to learn https://github.com/bitranox/wrapt_timeout_decorator/blob/master/wrapt_timeout_decorator/wrap_function_multiprocess.py """ # multiprocessing cannot run child with function defined in __main__ # https://stackoverflow.com/questions/43369648/cant-get-attribute-function-inner-on-module-mp-main-from-e-python # when the multiprocessing library copies your main module, it won't run it as the __main__ script # and therefore anything defined inside the if __name__ == '__main__' is not defined in the child # process namespace. Hence, the AttributeError # I tried to reload module or delay import module to work around this. neither worked. # re-import failed to work around # importlib.reload(multiprocessing) # importlib.reload(multiprocessing.connection) # delayed import failed to work around # import multiprocessing # import multiprocessing.connection # the parent process can always see the process defined in __main__ # tplog(getattr(sys.modules['__mp_main__'], 'local_sleep_and_tick')) parent_conn, child_conn = multiprocessing.Pipe(duplex=False) # how to pass args and kwargs # https://stackoverflow.com/questions/38908663/python-multiprocessing-how-to-pass-kwargs-to-function # p = multiprocessing.Process(target=timeout_child, args=(child_conn, func, *args), kwargs=kwargs) p = multiprocessing.Process(target=timeout_child, args=(child_conn, func, *args), kwargs=kwargs) # kill child after parent exits # https://stackoverflow.com/questions/25542110/kill-child-process-if-parent-is-killed-in-python p.daemon = True p.start() # Wait for timeout seconds or until process finishes p.join(timeout) # If thread is still active if p.is_alive(): message = f"timed out after {timeout} second(s), terminating pid={p.pid}" tplog(message) traceback.print_stack( ) # default to stderr, set file=sys.stdout for stdout # Terminate p.terminate() p.join(0.5) parent_conn.close() raise TimeoutException(timeout=timeout, child_pid=p.pid) else: result = None if parent_conn.poll(0.1): # recv() is a blocked call, therefore, poll() first result = parent_conn.recv() parent_conn.close() if isinstance(result, Exception): raise result else: # child finished and returned anything return result else: # child finished but didn't return anything parent_conn.close()
def send_and_encode(self, data: Union[bytes, str], data_is_file: bool = False, timeout: int = 6, **opt) -> int: """ this is actually use unblocked send() to re-implement a blocked sendall(). The possible benefits: - add encryption """ file = None fd = None if data_is_file: file = data size = os.path.getsize(file) fd = os.open(file, os.O_RDWR) # fd is int data = mmap.mmap(fd, size, access=mmap.ACCESS_READ) # tplog(f"data type = {type(data)}") # # https://docs.python.org/3/library/socket.html # socket.send() vs socket.sendall() # # socket.send(bytes[, flags]) # Send data to the socket. Returns the number of bytes sent. Applications are responsible # for checking that all data has been sent; if only some of the data was transmitted, the application # needs to attempt delivery of the remaining data. # # socket.sendall(bytes[, flags]) # Send data to the socket. Unlike send(), this method continues to # send data from bytes until either all data has been sent or an error occurs. None is returned on success. On # error, an exception is raised, and there is no way to determine how much data, if any, was successfully sent. saved_timeout = self.socket.gettimeout() # saved_blocking = self.socket.getblocking() this only available when version >= 3.7 # unblock self.socket.setblocking( 0 ) # None: blocking mode; 0: non-blocking mode; positive floating: timeout mode. # this is the same as self.socket.settimeout(0) polling_interval = 1 wait_so_far = 0 total_sent = 0 data_length = len(data) while total_sent < data_length: buffer_size = 4096 new_end = total_sent + buffer_size if new_end > data_length: new_end = data_length buffer_size = new_end - total_sent buffer_bytes = self.out_coder.xor(data[total_sent:new_end]) buffer_sent = 0 while buffer_sent < buffer_size: ready = select.select([], [self.socket], [], polling_interval) if ready[1]: sent = self.socket.send(buffer_bytes[buffer_sent:]) # there may be exceptions here if sent == 0: tplog(f"Remote connection is closed during our send()") break buffer_sent += sent total_sent += sent else: wait_so_far += polling_interval if wait_so_far >= timeout: tplog(f"send() timed out after {wait_so_far} seconds") break else: # the above loop did NOT break = the above loop condition was False = the loop ended naturally # python's way to break nested loop (double loop) # https://stackoverflow.com/questions/653509/breaking-out-of-nested-loops continue break # the above loop DID break = the above loop condition was still True = the loop didn't end naturally if fd: os.close(fd) missing = data_length - total_sent if missing > 0: tplog( f"Sent total {total_sent} bytes. failed to send {missing} bytes" ) else: tplog(f"Sent all {total_sent} bytes") # getblocking()/setblocking() is implemented by gettimeout()/settimeout. no getblocking() before 3.7 self.socket.settimeout(saved_timeout) # self.socket.setblocking(saved_blocking) # restoring blocking setting return total_sent
def recv_and_decode(self, timeout: int = 6, maxsize=1024 * 1024 * 1024, file: str = None, **opt) -> Union[bytes, int]: """ this is actually use unblocked recv() to re-implement a blocked recv(). The possible benefits: - sock.recv(buffer_size)'s buffer_size is limited, we can use a loop to recv() bigger file. In this loop pattern, socket.settimeout() looks awkward. socket.settimeout() seems better for small data - decrypt the incoming data with smaller chunks, ie, in buffer_size """ if file: # receive data will write to this file fh = open(file, 'wb') else: fh = None # https://docs.python.org/3/library/socket.html # getblocking()/setblocking() is implemented by gettimeout()/settimeout. no getblocking() before 3.7 saved_timeout = self.socket.gettimeout() # saved_blocking = self.socket.getblocking() this only available when version >= 3.7 # unblock self.socket.setblocking( 0 ) # None: blocking mode; 0: non-blocking mode; positive floating: timeout mode. polling_interval = 1 wait_so_far = 0 total_size = 0 received_bytearray = bytearray() while wait_so_far < timeout: ready = select.select( [self.socket], [], [], polling_interval) # last arg is timeout in seconds if ready[0]: data = self.socket.recv(4096) # there may be exceptions here if data == b'': tplog(f"Remote connection is closed during our recv()") break if fh: fh.write(self.in_coder.xor(data)) else: received_bytearray.extend(self.in_coder.xor(data)) total_size += len(data) if total_size > maxsize: tplog( f"Received {total_size} bytes already exceeded maxsize {maxsize}. Stopped receiving." ) break else: # time.sleep(polling_interval) # no need sleep here if we already blocked at select() wait_so_far += polling_interval if fh: fh.close() if wait_so_far >= timeout: tplog(f"recv() timed out after {wait_so_far} seconds") tplog(f"Received total {total_size} bytes") # getblocking()/setblocking() is implemented by gettimeout()/settimeout. no getblocking() before 3.7 self.socket.settimeout(saved_timeout) # self.socket.setblocking(saved_blocking) # restoring blocking setting if file: return total_size else: return bytes(received_bytearray)
def run(seleniumEnv: tpsup.seleniumtools.SeleniumEnv, **opt): verbose = opt.get('verbose', 0) mod_file = opt.get('mod_file', 'mod_file') argList = opt.get('argList', []) if verbose: sys.stderr.write(f"{mod_file} argList=\n") sys.stderr.write(pformat(argList) + "\n") parser = argparse.ArgumentParser(prog=mod_file, ) parser.add_argument('-js', dest='use_javascript', default=False, action='store_true', help='use javascript') parser.add_argument('-wait', dest='wait', default=5, action='store', type=int, help='rename the file. default not to rename') parser.add_argument('-rename', dest='renamed', default=None, action='store', help='rename the file. default not to rename') args = vars(parser.parse_args(argList)) if not verbose: verbose = args.get('verbose', 0) if verbose: tplog(f"args = {pformat(args)}") driver = seleniumEnv.get_driver() url = f"http://livingstonchinese.org/LCA2/index.php/join-us" driver.get(url) # https://stackoverflow.com/questions/46937319/how-to-use-chrome-webdriver-in-selenium-to-download-files-in-python driver.command_executor._commands["send_command"] = ( "POST", '/session/$sessionId/chromium/send_command') params = { 'cmd': 'Page.setDownloadBehavior', 'params': { 'behavior': 'allow', 'downloadPath': seleniumEnv.download_dir } } command_result = driver.execute("send_command", params) # https://dev.to/endtest/a-practical-guide-for-finding-elements-with-selenium-4djf # <a style="color: #1b57b1; font-weight: normal; text-decoration: none;" # href="/LCA2/images/docs/public/lca_bylaw_2019_11.pdf">lick to view LCA By Law</a> #elem = driver.find_element_by_css_selector("#content > div.item-page > div:nth-child(4) > pre:nth-child(15) > span > a") elem = driver.find_element_by_partial_link_text("view LCA By Law") src = elem.get_attribute("href") env = seleniumEnv.env download_dir = seleniumEnv.download_dir shortname = "lca.pdf" urllib.request.urlretrieve(src, f"{download_dir}/{shortname}") return download_dir
sys.exit(1) tmpdir = tpsup.tptmp.tptmp().get_nowdir() # this is client mode if serverHostPort is defined ensock = tpsup.nettools.encryptedsocket(key, host_port=serverHostPort) request = { 'mod_file': args['mod_file'], 'args': args['remainingArgs'], 'accept': args['accept'], } request_str = json.dumps(request) request_bytes = bytes(request_str, "utf-8") tplog(f"Sending {len(request_bytes)} bytes", file=sys.stderr) ensock.send_and_encode(request_bytes) # shut down the send channel so that the other side recv() won't wait forever ensock.send_shutdown() tplog("Sent. waiting response") if request['accept'] == 'json': received_bytes = ensock.recv_and_decode( timeout=60) # this needs a long wait tplog(f"received {received_bytes} bytes") received_str = str(received_bytes, 'utf-8') received_structure = json.loads(received_str) tplog(f"Received data structure: {pformat(received_structure)}") elif request['accept'] == 'tar': tar_name = os.path.join(tmpdir, "reply.tar")
def run(seleniumEnv: tpsup.seleniumtools.SeleniumEnv, **opt): username = "******" # change this to the username associated with your account verbose = opt.get('verbose', 0) mod_file = opt.get('mod_file', 'mod_file') argList = opt.get('argList', []) if verbose: sys.stderr.write(f"{mod_file}argList=\n") sys.stderr.write(pformat(argList) + "\n") parser = argparse.ArgumentParser(prog=mod_file, ) parser.add_argument('-u', dest='username', default=username, action='store', help='login user') args = vars(parser.parse_args(argList)) if not verbose: verbose = args.get('verbose', 0) if verbose: tplog(f"args = {pformat(args)}") username = args['username'] entryBook = EntryBook() password = entryBook.get_entry_by_key(username).get('decoded') driver = seleniumEnv.get_driver() # print(f'driver.title={driver.title}') url = 'https://livingstonchinese.org/' driver.get(url) expected_url = "https://livingstonchinese.org/LCA2/" actual_url = driver.current_url assert expected_url == actual_url seleniumEnv.delay_for_viewer() # give 1 sec to let the tail set up # https://dev.to/endtest/a-practical-guide-for-finding-elements-with-selenium-4djf # in chrome browser, find the interested spot, right click -> inspect, this will bring up source code, # in the source code window, right click -> copy -> ... # from Edge/Chrome, right click the item -> inspect login_elem = driver.find_element_by_id('modlgn-username') login_elem.send_keys(username) seleniumEnv.delay_for_viewer(1) # delay to mimic humane slowness password_elem = driver.find_element_by_id('modlgn-passwd') password_elem.send_keys(password) # frameinfo = getframeinfo(currentframe()) # print(frameinfo.filename, frameinfo.lineno, file=sys.stderr) # print line number # from Edge/Chrome, right click the item -> inspect # because login button has no "id", so I used xpath. xpath is very sensitive to changes in the page driver.find_element_by_xpath( '/html/body/div[1]/div/div/div/div[1]/form/div/div[4]/div/button' ).click() seleniumEnv.delay_for_viewer(1) # delay to mimic humane slowness # this doesn't work as 'button' is a grand-child of form-login-sutmit # driver.find_element_by_id('form-login-submit').click() # frameinfo = getframeinfo(currentframe()) # print(frameinfo.filename, frameinfo.lineno, file=sys.stderr) xpath = '//*[@id=\"login-form\"]' elem = driver.find_element_by_xpath('//*[@id=\"login-form\"]') welcomeText = elem.text # frameinfo = getframeinfo(currentframe()) # print(frameinfo.filename, frameinfo.lineno, file=sys.stderr) print(f"We see: {welcomeText}") # assert re.search("^Hi ", welcomeText) error = None pattern = "^Hi " if not re.search(pattern, welcomeText): error = f"xpath='{xpath}' failed to match pattern '{pattern}'" result = {'error': error, 'data': welcomeText} return result
def run(seleniumEnv: tpsup.seleniumtools.SeleniumEnv, **opt): verbose = opt.get('verbose', 0) mod_file = opt.get('mod_file', 'mod_file') argList = opt.get('argList', []) if verbose: sys.stderr.write(f"{mod_file} argList=\n") sys.stderr.write(pformat(argList) + "\n") parser = argparse.ArgumentParser( prog=mod_file, ) parser.add_argument( '-js', dest='use_javascript', default=False, action='store_true', help='use javascript') parser.add_argument( '-wait', dest='wait', default=5, action='store', type=int, help='rename the file. default not to rename') parser.add_argument( '-rename', dest='renamed', default=None, action='store', help='rename the file. default not to rename') args = vars(parser.parse_args(argList)) if not verbose: verbose = args.get('verbose',0) if verbose: tplog(f"args = {pformat(args)}") driver = seleniumEnv.get_driver() url = f"https://metacpan.org/pod/DBI::Log" tplog(f"going to url={url}") driver.get(url) # https://stackoverflow.com/questions/46937319/how-to-use-chrome-webdriver-in-selenium-to-download-files-in-python driver.command_executor._commands["send_command"] = ("POST", '/session/$sessionId/chromium/send_command') params = {'cmd': 'Page.setDownloadBehavior', 'params': {'behavior': 'allow', 'downloadPath': seleniumEnv.download_dir}} command_result = driver.execute("send_command", params) time.sleep(1) # https://dev.to/endtest/a-practical-guide-for-finding-elements-with-selenium-4djf download_text = 'Download (' elem = driver.find_element_by_partial_link_text(download_text) elem.click() tplog(f"clicked '{download_text}'") wait_time_for_download = float(args['wait']) time.sleep(wait_time_for_download) # control file name # https://stackoverflow.com/questions/34548041/selenium-give-file-name-when-downloading files: List[str] = [os.path.join(seleniumEnv.download_dir, f) for f in os.listdir(seleniumEnv.download_dir)] if files: filename = max(files, key=os.path.getctime) renamed = args['renamed'] if renamed: shutil.move(filename, os.path.join(seleniumEnv.download_dir, renamed)) return renamed else: return filename else: tplog(f"no file downloaded to {seleniumEnv.download_dir}") raise RuntimeError(f"no file downloaded in {wait_time_for_download} seconds")
def run(seleniumEnv: tpsup.seleniumtools.SeleniumEnv, **opt): username = "******" # change this to the username associated with your account verbose = opt.get('verbose', 0) mod_file = opt.get('mod_file', 'mod_file') argList = opt.get('argList', []) if verbose: sys.stderr.write(f"{mod_file}argList=\n") sys.stderr.write(pformat(argList) + "\n") parser = argparse.ArgumentParser(prog=mod_file, ) parser.add_argument('-u', dest='username', default=username, action='store', help='login user') args = vars(parser.parse_args(argList)) if not verbose: verbose = args.get('verbose', 0) if verbose: tplog(f"args = {pformat(args)}") username = args['username'] entryBook = EntryBook() password = entryBook.get_entry_by_key(username).get('decoded') driver = seleniumEnv.get_driver() # print(f'driver.title={driver.title}') url = 'https://livingstonchinese.org/' driver.get(url) expected_url = "https://livingstonchinese.org/LCA2/" actual_url = driver.current_url assert expected_url == actual_url seleniumEnv.delay_for_viewer() # give 1 sec to let the tail set up # https://dev.to/endtest/a-practical-guide-for-finding-elements-with-selenium-4djf # in chrome browser, find the interested spot, right click -> inspect, this will bring up source code, # in the source code window, right click -> copy -> ... found_logout = True need_to_login = False logout_elem = None logout_css_selector = '#login-form > div.logout-button > input.btn.btn-primary' try: logout_elem = driver.find_element_by_css_selector(logout_css_selector) except NoSuchElementException: found_logout = False if found_logout: tplog( f"found logout button, meaning someone already logged in. css_selector='{logout_css_selector}'" ) need_to_logout = True greeting_text = None greeting_css = '#login-form > div.login-greeting' try: greeting_elem = driver.find_element_by_css_selector(greeting_css) if verbose: attrs = seleniumEnv.get_attrs(greeting_elem, method='bs4') tplog(f"attrs by bs4 = {pformat(attrs)}") attrs = seleniumEnv.get_attrs(greeting_elem, method='js') tplog(f"attrs by js = {pformat(attrs)}") greeting_text = greeting_elem.text tplog(f"greeting_text='{greeting_text}' at css='{greeting_css}'") except Exception as e: print_exception(e) tplog( f"cannot find greeting_text at css='{greeting_css}'. need to log out" ) if greeting_text: greeting_pattern = "^Hi (.+)," m = re.search(greeting_pattern, greeting_text) if m: username = m.group(1) expected_username = "******" if not username == expected_username: tplog( f"username='******' did not match expected username='******'. need to log out" ) else: need_to_logout = False tplog( f"username='******' matched expected username='******'. no need to log in again" ) else: tplog( f"greeting_text='{greeting_text}', not matching expected greeting_pattern='{greeting_pattern}'. " f"need to log out") else: tplog( f"cannot find greeting_text at css_selector={greeting_css}. need to log out" ) if need_to_logout: need_to_login = True logout_elem.click() seleniumEnv.delay_for_viewer(1) else: tplog(f"seems nobody logged in. we will login.") need_to_login = True if need_to_login: # from Edge/Chrome, right click the item -> inspect login_elem = driver.find_element_by_id('modlgn-username') login_elem.send_keys(username) seleniumEnv.delay_for_viewer(1) # delay to mimic humane slowness password_elem = driver.find_element_by_id('modlgn-passwd') password_elem.send_keys(password) # frameinfo = getframeinfo(currentframe()) # print(frameinfo.filename, frameinfo.lineno, file=sys.stderr) # print line number # from Edge/Chrome, right click the item -> inspect # because login button has no "id", so I used xpath. xpath is very sensitive to changes in the page driver.find_element_by_xpath( '/html/body/div[1]/div/div/div/div[1]/form/div/div[4]/div/button' ).click() seleniumEnv.delay_for_viewer(1) # delay to mimic humane slowness # this doesn't work as 'button' is a grand-child of form-login-sutmit # driver.find_element_by_id('form-login-submit').click() # frameinfo = getframeinfo(currentframe()) # print(frameinfo.filename, frameinfo.lineno, file=sys.stderr) xpath = '//*[@id=\"login-form\"]' elem = driver.find_element_by_xpath('//*[@id=\"login-form\"]') welcomeText = elem.text # frameinfo = getframeinfo(currentframe()) # print(frameinfo.filename, frameinfo.lineno, file=sys.stderr) tplog(f"We see: {welcomeText}") # assert re.search("^Hi ", welcomeText) error = None pattern = "^Hi " if not re.search(pattern, welcomeText): error = f"xpath='{xpath}' failed to match pattern '{pattern}'" result = {'error': error, 'data': welcomeText} return result