Managing multiple instances of Selenium in Python - python

I am trying to be able to manage multiple instances of Selenium at the same time, but haven't had much luck. I'm not 100% sure if it's possible. I have an application with a GUI built with PyQT that retrieves our client's information from our SQL database. It's a fairly simple app that lets our users easily log in and out of our clients' accounts. They click the client's name, press "Login", it launches an instance of Firefox, logs into the account, and stays open so the user can do whatever they need to do. When they are done, they click the "Logout" button, and it logs out of the account and quits the webdriver instance.
What I'm trying to provide is a way for them to log into multiple accounts at once, while still maintaining the ability to click one of the client's names that they are logged into, process the logout on that account, and close that browser instance.
One thing I was hoping is to be able to control the webdriver by either a process ID, or unique ID, in which I can store in a dictionary linking it to that client, so when they click the client's name in the app, and press logout, it uses something in PyQT like "client_name = self.list_item.currentItem().text()" to get the name of the client they have selected (which I'm already using for other things, too), finds the unique ID or process ID, and sends the logout command to that instance, and then closes that instance.
This may not be the best way to go about doing it, but it's the only thing I could think of.
EDIT: I also know that you can retrieve the Selenium session_id with driver.session_id (considering your webdriver instance is assigned as 'driver'), but i have seen nothing so far on being able to control a webdriver instance by this session_id.
EDIT2: Here is an incredibly stripped down version of what I have:
from selenium import webdriver
from PyQt4 import QtGui, QtCore
class ClientAccountManager(QtGui.QMainWindow):
def __init__(self):
super(ClientAccountManager, self).__init__()
grid = QtGui.QGridLayout()
# Creates the list box
self.client_list = QtGui.QListWidget(self)
# Populates the list box with owner data
for name in client_names.itervalues():
item = QtGui.QListWidgetItem(name)
self.client_list.addItem(item)
# Creates the login button
login_btn = QtGui.QPushButton("Login", self)
login_btn.connect(login_btn, QtCore.SIGNAL('clicked()'), self.login)
# Creates the logout button
logout_btn = QtGui.QPushButton("Logout", self)
logout_btn.connect(logout_btn, QtCore.SIGNAL('clicked()'), self.logout)
def login(self):
# Finds the owner info based on who is selected
client_name = self.client_list.currentItem().text()
client_username, client_password = get_credentials(client_name)
# Creates browser instance
driver = webdriver.Firefox()
# Logs in
driver.get('https://www.....com/login.php')
driver.find_element_by_id('userNameId').send_keys(client_username)
driver.find_element_by_id('passwordId').send_keys(client_password)
driver.find_element_by_css_selector('input[type=submit]').click()
def logout(self):
# Finds the owner info based on who is selected
client_name = self.client_list.currentItem().text()
# Logs out
driver.get('https://www....com/logout.php')
# Closes the browser instance
driver.quit()
def main():
app = QtGui.QApplication(sys.argv)
cpm = ClientAccountManager()
cpm.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()

You can have multiple drivers. Just call webdriver.Firefox() multiple times and keep references to each driver. Some people report oddball behavior, but it basically works.
driver.close() will close the browser and does not take an id.

Related

How to open web API calls to the default browser window in PyQt5 Python

I have a browser code built in python using PyQt5 and I want to implement a functionality which is whenever the browser receives an API call from a website to some other service it should open that in the default browser.
For example, whenever we want to login to a website using google and we click on the google option, we get a new window to select our google account. That's the same functionality I want to implement.
Find the complete code here: https://pastebin.com/41n9eghQ
class WebPage(QWebEnginePage):
linkClicked = Signal(QUrl)
def acceptNavigationRequest(self, url, navigation_type, isMainFrame):
if navigation_type == QWebEnginePage.NavigationTypeLinkClicked:
self.linkClicked.emit(url)
return False
return super(WebPage, self).acceptNavigationRequest(
url, navigation_type, isMainFrame)
The above class inside the code accepts and processes the in-browser navigation requests.
def createWindow(self, webwindowtype):
import webbrowser
try:
webbrowser.open(to_text_string(self.url().toString()))
except ValueError:
pass
The above function opens the window in default browser whenever an API call gets triggered, the only problem here is right now I am passing the url of the current website i.e. self.url() and not the url of the API and I don't understand how to do so.
Is there a way to capture the API request and pass that url to the webbrowser.open() function.
Any help would be appreciated, thanks for your attention!

Downloading PDF's and tracking downloads with Python

I'm creating an application that downloads PDF's from a website and saves them to disk. I understand the Requests module is capable of this but is not capable of handling the logic behind the download (File size, progress, time remaining etc.).
I've created the program using selenium thus far and would like to eventually incorporate this into a GUI Tkinter app eventually.
What would be the best way to handle the downloading, tracking and eventually creating a progress bar?
This is my code so far:
from selenium import webdriver
from time import sleep
import requests
import secrets
class manual_grabber():
""" A class creating a manual downloader for the Roger Technology website """
def __init__(self):
""" Initialize attributes of manual grabber """
self.driver = webdriver.Chrome('\\Users\\Joel\\Desktop\\Python\\manual_grabber\\chromedriver.exe')
def login(self):
""" Function controlling the login logic """
self.driver.get('https://rogertechnology.it/en/b2b')
sleep(1)
# Locate elements and enter login details
user_in = self.driver.find_element_by_xpath('/html/body/div[2]/form/input[6]')
user_in.send_keys(secrets.username)
pass_in = self.driver.find_element_by_xpath('/html/body/div[2]/form/input[7]')
pass_in.send_keys(secrets.password)
enter_button = self.driver.find_element_by_xpath('/html/body/div[2]/form/div/input')
enter_button.click()
# Click Self Service Area button
self_service_button = self.driver.find_element_by_xpath('//*[#id="bs-example-navbar-collapse-1"]/ul/li[1]/a')
self_service_button.click()
def download_file(self):
"""Access file tree and navigate to PDF's and download"""
# Wait for all elements to load
sleep(3)
# Find and switch to iFrame
frame = self.driver.find_element_by_xpath('//*[#id="siteOutFrame"]/iframe')
self.driver.switch_to.frame(frame)
# Find and click tech manuals button
tech_manuals_button = self.driver.find_element_by_xpath('//*[#id="fileTree_1"]/ul/li/ul/li[6]/a')
tech_manuals_button.click()
bot = manual_grabber()
bot.login()
bot.download_file()
So in summary, I'd like to make this code download PDF's on a website, store them in a specific directory (named after it's parent folder in the JQuery File Tree) and keep tracking of the progress (file size, time remaining etc.)
Here is the DOM:
I hope this is enough information. Any more required please let me know.

How create local notification on MacOS Catalina pyobjc?

I am having some difficulty finding out how to send local notifications on Catalina using pyobjc.
The closes example I have seen is this:
PyObjC "Notifications are not allowed for this application"
Edit (June 27, 2020): I've created a package which has functionality to display notifications on Mac OS here. It will use PyObjC to create and display notifications. If It does not work for whatever reason, it will fallback to AppleScript notifications with osascript. I did some testing and found that the PyObjC notifications work on some devices but don't on some.
Answer:
I have also been searching for this answer, so I'd like to share what I've found:
The first thing you'll notice is that the function notify() defines a class, then returns an instance of it. You might be wondering why you can't directly call Notification.send(params). I tried it, but I was getting an error with the PyObjC, which I am unfortunately unable to fix:
# Error
class Notification(NSObject):
objc.BadPrototypeError: Objective-C expects 1 arguments, Python argument has 2 arguments for <unbound selector send of Notification at 0x10e410180>
Now onto the code:
# vscode may show the error: "No name '...' in module 'Foundation'; you can ignore it"
from Foundation import NSUserNotification, NSUserNotificationCenter, NSObject, NSDate
from PyObjCTools import AppHelper
def notify(
title='Notification',
subtitle=None, text=None,
delay=0,
action_button_title=None,
action_button_callback=None,
other_button_title=None,
other_button_callback=None,
reply_placeholder=None,
reply_callback=None
):
class Notification(NSObject):
def send(self):
notif = NSUserNotification.alloc().init()
if title is not None:
notif.setTitle_(title)
if subtitle is not None:
notif.setSubtitle_(subtitle)
if text is not None:
notif.setInformativeText_(text)
# notification buttons (main action button and other button)
if action_button_title:
notif.setActionButtonTitle_(action_button_title)
notif.set_showsButtons_(True)
if other_button_title:
notif.setOtherButtonTitle_(other_button_title)
notif.set_showsButtons_(True)
# reply button
if reply_callback:
notif.setHasReplyButton_(True)
if reply_placeholder:
notif.setResponsePlaceholder_(reply_placeholder)
NSUserNotificationCenter.defaultUserNotificationCenter().setDelegate_(self)
# setting delivery date as current date + delay (in seconds)
notif.setDeliveryDate_(NSDate.dateWithTimeInterval_sinceDate_(delay, NSDate.date()))
# schedule the notification send
NSUserNotificationCenter.defaultUserNotificationCenter().scheduleNotification_(notif)
# on if any of the callbacks are provided, start the event loop (this will keep the program from stopping)
if action_button_callback or other_button_callback or reply_callback:
print('started')
AppHelper.runConsoleEventLoop()
def userNotificationCenter_didDeliverNotification_(self, center, notif):
print('delivered notification')
def userNotificationCenter_didActivateNotification_(self, center, notif):
print('did activate')
response = notif.response()
if notif.activationType() == 1:
# user clicked on the notification (not on a button)
# don't stop event loop because the other buttons can still be pressed
pass
elif notif.activationType() == 2:
# user clicked on the action button
action_button_callback()
AppHelper.stopEventLoop()
elif notif.activationType() == 3:
# user clicked on the reply button
reply_text = response.string()
reply_callback(reply_text)
AppHelper.stopEventLoop()
# create the new notification
new_notif = Notification.alloc().init()
# return notification
return new_notif
def main():
n = notify(
title='Notification',
delay=0,
action_button_title='Action',
action_button_callback=lambda: print('Action'),
# other_button_title='Other',
# other_button_callback=lambda: print('Other'),
reply_placeholder='Enter your reply please',
reply_callback=lambda reply: print('Replied: ', reply),
)
n.send()
if __name__ == '__main__':
main()
Explanation
The notify() function takes in quite a few parameters (they are self-explanatory). The delay is how many seconds later the notification will appear. Note that if you set a delay that's longer than the execution of the program, the notification will be sent ever after the program is being executed.
You'll see the button parameters. There are three types of buttons:
Action button: the dominant action
Other button: the secondary action
Reply button: the button that opens a text field and takes a user input. This is commonly seen in messaging apps like iMessage.
All those if statements are setting the buttons appropriately and self explanatory. For instance, if the parameters for the other button are not provided, a Other button will not be shown.
One thing you'll notice is that if there are buttons, we are starting the console event loop:
if action_button_callback or other_button_callback or reply_callback:
print('started')
AppHelper.runConsoleEventLoop()
This is a part of Python Objective-C. This is not a good explanation, but it basically keeps program "on" (I hope someone cane give a better explanation).
Basically, if you specify that you want a button, the program will continue to be "on" until AppHelper.stopEventLoop() (more about this later).
Now there are some "hook" functions:
userNotificationCenter_didDeliverNotification_(self, notification_center, notification): called when the notification is delivered
userNotificationCenter_didActivateNotification_(self, notification_center, notification): called when the user interacts with the notification (clicks, clicks action button, or reply) (documentation)
There surely are more, but I do not think there is a hook for the notification being dismissed or ignored, unfortunately.
With userNotificationCenter_didActivateNotification_, we can define some callbacks:
def userNotificationCenter_didActivateNotification_(self, center, notif):
print('did activate')
response = notif.response()
if notif.activationType() == 1:
# user clicked on the notification (not on a button)
# don't stop event loop because the other buttons can still be pressed
pass
elif notif.activationType() == 2:
# user clicked on the action button
# action button callback
action_button_callback()
AppHelper.stopEventLoop()
elif notif.activationType() == 3:
# user clicked on the reply button
reply_text = response.string()
# reply button callback
reply_callback(reply_text)
AppHelper.stopEventLoop()
There are different activation types for the types of actions. The text from the reply action can also be retrieved as shown.
You'll also notice the AppHelper.stopEventLoop() at the end. This means to "end" the program from executing, since the notification has been dealt with by the user.
Now let's address all the problems with this solution.
Problems
The program will never stop if the user does not interact with the notification. The notification will slide away into the notification center and may or may never be interacted with. As I stated before, there's no hook for notification ignored or notification dismissed, so we cannot call AppHelper.stopEventLoop() at times like this.
Because AppHelper.stopEventLoop() is being run after interaction, it is not possible to send multiple notifications with callbacks, as the program will stop executing after the first notification is interacted with.
Although I can show the Other button (and give it text), I couldn't find a way to give it a callback. This is why I haven't addressed it in the above code block. I can give it text, but it's essentially a dummy button as it cannot do anything.
Should I still use this solution?
If you want notifications with callbacks, you probably should not, because of the problems I addressed.
If you only want to show notifications to alert the user on something, yes.
Other solutions
PYNC is a wrapper around terminal-notifier. However, both received their last commit in 2018. Alerter seems to be a successor to terminal-notifier, but there is not Python wrapper.
You can also try running applescript to send notifications, but you cannot set callbacks, nor can you change the icon.
I hope this answer has helped you. I am also trying to find out how to reliably send notifications with callbacks on Mac OS. I've figured out how to send notifications, but callbacks is the issue.

PyQT : How to pass a webdriver object from QThread to UI thread?

I'm making a program that automatically enters to a website and login(basically using selenium and chrome webdriver). User can type own information(id&pw) and site address in a dialog(used PyQt4 module). After finishing it, pressing ok button will execute it.
After logging in, I want to do some other actions with that webdriver objects.
So my question is, how can I pass the webdriver object to the main thread(which is UI thread here) so that I can do some other actions(such as logging out, etc), or how to manage that webdriver object generated in other thread in main thread?
I'm using Python3.7.4 and PyQt4 version.
I googled similar questions and found that it might be related with signal&slots.
so I tried to imitate this example (https://nikolak.com/pyqt-threading-tutorial/) which uses custom signal.
In this example, it passes a QString instance to the main thread(UI thread).
So I tried to pass my webdriver object by imitating it, but It's not going well...
The basic structure of code is following:
class MyDialog(QDialog):
def __init__(self):
QDialog.__init__(self)
# and some codes with widget layouts
def btnOkClicked(self):
a = [self.editSite1.text(), self.editId.text(), self.editPw.text()]
self.gothread = goWebsiteThread(a)
# 'goWebsiteThread' is a thread that generates webdriver object and executes login function
self.connect(self.gothread, SIGNAL("add_driver(PyQt_PyObject)"), self.add_driver)
# this line is what I tried to pass the driver object to this main thread
self.gothread.start()
class goWebsiteThread(QThread, QObject):
# I tried to pass this class's object by making this class inherit QObject class... sorry for unfounded try..
def __init__(self, sites):
QThread.__init__(self)
self.sites = sites
def goWebsite(self):
self.driver = webdriver.Chrome('./chromedriver.exe', options=options)
self.driver.get(some site address that user typed)
# and codes to log in
self.emit(SIGNAL('add_driver(PyQt.PyObject)'), self.driver)
# I tried to pass the driver object by emitting signal...
def run(self):
self.goWebsite()
But this doesn't work (MyDialog object doesn't recognize the driver object).
How can I properly pass the webdriver object to MyDialog object and use it?
Webdriver should run on a new thread and it would be impossible to control webdriver from a UI thread.
But you can still store the webdriver as a member variable of the UI instance.
If you want to send some commands to the webdriver, you would need to start a new thread and handle automation works in the newly created thread.

QWebInspector: How to switch tab manually

When I use QWebInspector (python, PyQT4), it always opens with default activated "Elements" tab.
Is there a way programmatically switch tab to Network?
Now it looks as:
What I want to see:
Script source:
import sys, PyQt4.QtCore, PyQt4.QtGui, PyQt4.QtWebKit
if __name__ == '__main__':
app = PyQt4.QtGui.QApplication(sys.argv)
webview = PyQt4.QtWebKit.QWebView()
inspector = PyQt4.QtWebKit.QWebInspector()
webview.page().settings().setAttribute(
PyQt4.QtWebKit.QWebSettings.DeveloperExtrasEnabled, True
)
inspector.setPage(webview.page())
inspector.showMaximized()
webview.load(PyQt4.QtCore.QUrl('http://yahoo.com'))
app.exec_()
Surely, appropriate ะก++/QT method is also suitable!
Thanks!
WebInspector is based on web. You will need to get first and only child of inspector, it will be QWebView.
Like that:
QList<QWidget*> list = inspector.findChildren<QWidget *>();
QWebView* wv =(QWebView*) list.at(0);
Then connect to javaScriptWindowObjectCleared signal of this view, and in the connected slot execute javascript. You will need object to handle this. I called it someObject, for example
QObject::connect(
wv->page()->mainFrame(),
SIGNAL(javaScriptWindowObjectCleared()),
someObject,
SLOT(openNetworkTab())
);
add slot to someObject's class:
void openNetworkTab(){
wv->page()->mainFrame()->evaluateJavaScript("document.addEventListener('DOMContentLoaded',function(){setTimeout(function(){document.querySelector('.toolbar-item.network').click()},200);});");
}
Delay 200 just to wait for all event bindings initialization before make click
Here list of all inspector tab classes, just in case: .elements, .resources, .network, .scripts, .timeline, .profiles, .audits, .console

Categories

Resources