Combining Asyncio library (telethon) with Pyqt - python

I want to combine a TelegramBot with a PYQT application. I'm using telethon to listen to a channel and print out messages. However, this should run in the background, that is, the user should be able to interact with the GUI while the bot is listening to messages. So far, I've only managed to implement a blocking variant. That is, the gui is not showing because with client seems to be blocking.
class Win(QtWidgets.QMainWindow):
def __init__(self):
QtWidgets.QMainWindow.__init__(self)
self.ui = uic.loadUi("layout.ui", self)
client = TelegramClient('session', TELE_ID, TELE_HASH)
#client.on(events.NewMessage(chats=["test"]))
async def m_listener(event):
print(event)
with client:
client.run_until_disconnected()
I also tried to move the last statement to a thread with
...
def run():
with client:
client.run_until_disconnected()
threading.Thread(target=run).start()
However, then I get a There is no current event loop in thread 'Thread-1'. error because with client tries to load the loop from asyncio which can only be done in the main thread.
Do you can give me any starting point on how to solve this?

It needs to create new async loop inside thread - so it needs to create new Client also in thread.
import asyncion
def run_bot():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
#client = TelegramClient('session', TELE_ID, TELE_HASH, loop=loop)
client = TelegramClient('session', TELE_ID, TELE_HASH) # also works
# ... code ...
with client:
client.run_until_disconnected()
# ---
threading.Thread(target=run_bot).start()
Working example which also use Queue (queue_in) to send command to bot.
It has also queue_out to send message from bot to PyQt but I didn't create code which could use this message.
PyQt has own class QThread which could be more useful because it uses singals to send messages.
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QPushButton
from PyQt5.QtCore import Qt
#from PyQt5.QtCore import QThread
from telethon.sync import TelegramClient, events
import asyncio
from queue import Queue
from threading import Thread
import os
TOKEN = os.getenv('TELEGRAM_TOKEN')
TELE_ID, TELE_HASH = TOKEN.split(":")
# --- classes ---
class MyWindow(QWidget):
def __init__(self, queue_in, queue_out):
super().__init__()
self.queue_in = queue_in
self.queue_out = queue_out
self.label = QLabel(self, text='Hello World', alignment=Qt.AlignCenter)
self.button = QPushButton(self, text='Stop Bot')
self.button.clicked.connect(self.stop_bot)
vbox = QVBoxLayout()
vbox.addWidget(self.label)
vbox.addWidget(self.button)
self.setLayout(vbox)
def stop_bot(self):
self.queue_in.put('stop')
# --- functions ---
def run_bot(queue_in, queue_out):
print("run_bot()")
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
print('loop:', loop)
client = TelegramClient('session', TELE_ID, TELE_HASH)#, loop=loop)
#client.on(events.NewMessage(pattern='/test'))
async def test_cmd(event):
#print('[BOT] event:', event)
print('[BOT] message:', event.message.message)
queue_out.put(event.message.message)
#client.on(events.NewMessage(pattern='/stop'))
async def stop_cmd(event):
print('[BOT] message:', event.message.message)
queue_out.put(event.message.message)
await client.disconnect() # it stops client
async def check_queue():
print('[BOT] check_queue(): start')
while True:
await asyncio.sleep(1)
#print('[BOT] check_queue(): check')
if not queue_in.empty():
cmd = queue_in.get()
print('[BOT] check_queue(): queue_in get:', cmd)
if cmd == 'stop':
await client.disconnect()
break
loop.create_task(check_queue())
with client:
print('[BOT] start')
client.run_until_disconnected()
print('[BOT] stop')
# --- main ---
queue_in = Queue() # to send data to bot
queue_out = Queue() # to receive data from bot
thread = Thread(target=run_bot, args=(queue_in, queue_out))
thread.start()
# - GUI -
print('PyQt')
app = QApplication([])
win = MyWindow(queue_in, queue_out)
win.show()
app.exec()
queue_in.put('stop')
thread.join()

Related

Python asyncio listener loop doesn't run using idle main loop

I have a "listener" loop that constantly watches for items to process from an asyncio queue. This loop runs in a part of the application that is not using asyncio, so I've been trying to set up a passive asyncio main loop that the listener can be transferred to as needed. The listener is started and stopped as needed per input from the user.
For some reason the code below never results in the listener() actually running (i.e. print("Listener Running") is never printed). start_IOLoop_thread is run at startup of the application.
Can anyone point out what the problem is with this setup? Please let me know if more info is needed.
Edit: replaced code with a runnable example per the comments:
import asyncio
import threading
from asyncio.queues import Queue
import time
class Client:
def __init__(self):
self.streamQ = Queue()
self.loop = None
self.start_IOLoop_thread()
self.stream_listener()
def stream_listener(self):
self.streaming = True
async def listener():
print("Listener Running")
while self.streaming:
data = await self.streamQ.get()
# DEBUG
print(data)
print("Listener Stopped")
print("Starting Listener")
self.listener = asyncio.run_coroutine_threadsafe(listener(), self.loop)
def start_IOLoop_thread(self):
async def inf_loop():
# Keep the main thread alive and doing nothing
# so we can freely give it tasks as needed
while True:
await asyncio.sleep(1)
async def main():
await inf_loop()
def start_IO():
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
asyncio.run(main())
print("Main Exited")
threading.Thread(target=start_IO, daemon=True).start()
# A small delay is needed to give the loop time to initialize,
# otherwise self.loop is passed as "None"
time.sleep(0.1)
if __name__ == "__main__":
C = Client()
input("Enter to exit")
You never start the newly created loop. I adjusted to call main (although here it does nothing I assume the original code is more complex). All changes are in start_IO. Tested with python 3.10 (I think there was some change in the past regarding threads and async)
import asyncio
import threading
from asyncio.queues import Queue
import time
class Client:
def __init__(self):
self.streamQ = Queue()
self.loop = None
self.start_IOLoop_thread()
self.stream_listener()
def stream_listener(self):
self.streaming = True
async def listener():
print("Listener Running")
while self.streaming:
data = await self.streamQ.get()
# DEBUG
print(data)
print("Listener Stopped")
print("Starting Listener")
self.listener = asyncio.run_coroutine_threadsafe(listener(), self.loop)
def start_IOLoop_thread(self):
async def inf_loop():
# Keep the main thread alive and doing nothing
# so we can freely give it tasks as needed
while True:
await asyncio.sleep(1)
async def main():
await inf_loop()
def start_IO():
self.loop = asyncio.new_event_loop()
self.loop.create_task(main())
asyncio.set_event_loop(self.loop)
self.loop.run_forever()
print("Main Exited")
threading.Thread(target=start_IO, daemon=True).start()
# A small delay is needed to give the loop time to initialize,
# otherwise self.loop is passed as "None"
time.sleep(0.1)
if __name__ == "__main__":
C = Client()
input("Enter to exit")

Running a class in a separate thread

I can't launch the browser in a separate thread.
I have this code:
import asyncio
from pyppeteer import launch
class browser(QThread):
def __init__(self):
QThread.__init__(self)
def run(self):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(self.main())
async def main(self):
url = 'https://example.com'
browser = await launch(headless=True)
page = await browser.newPage()
await page.goto(url, waitUntil='networkidle0')
When calling, this error comes out:
handler = _signal.signal(_enum_to_int(signalnum), _enum_to_int(handler))
ValueError: signal only works in main thread of the main interpreter
If you run it without QThread then everything works

aiohttp Server Stops Immediately

I am trying to run a simple aiohttp server that can be stopped by the user hitting CTRL+C.
However, right now it appears to stop immediately with no error messages after starting. What might be the problem?
import asyncio
from aiohttp import web
class Foo:
def __init__(self):
self.app = web.Application()
self.app.add_routes([])
self.runner = web.AppRunner(self.app)
async def start(self):
await self.runner.setup()
site = web.TCPSite(self.runner, "127.0.0.1", 1234)
await site.start()
async def stop(self):
await self.runner.cleanup()
async def main():
await foo.start()
if __name__ == '__main__':
foo = Foo()
try:
asyncio.run(main())
except KeyboardInterrupt:
asyncio.run(foo.stop())

How do I add asyncio task to pyqt5 event loop so that it runs and avoids the never awaited error?

I am new to asyncio and I want to leverage it for my pyqt application to handle communication over a tcp connection.
I made this trivial demo to learn how to deal with the QT loop in asyncio context. I have seen other post related to this but they are much more complicated than what I am trying to do at the moment. So I start the server client in a separate window so it listens and I try to send a message through my simple button click event on my widget. As barebones as it gets.... My problem is it does not work.
I am looking to be able to a single exchange of info and a case where the port is left open for a stream. I think these tasks would be straight forward in asyncio but having it play nice with qt seems difficult at this point.
right now I am getting
RuntimeWarning: coroutine 'PushButton.sendmessage' was never awaited
rslt = self.__app.exec_()
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
I am not sure where to start on fixing this.
test.py
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton
from PyQt5.QtGui import QIcon
from PyQt5.QtCore import Qt, pyqtSlot
import sys
import asyncio
from asyncqt import QEventLoop
from async_client import tcp_echo_client
class PushButton(QWidget):
loop = None
def __init__(self,app_loop):
super(PushButton,self).__init__()
self.initUI()
self.loop = loop
def initUI(self):
self.setWindowTitle("PushButton")
self.setGeometry(400,400,300,260)
self.send_bttn = QPushButton(self)
self.send_bttn.setText("SEnd Message")
self.send_bttn.clicked.connect(self.sendmessage)
self.send_bttn.move(100,100)
#pyqtSlot()
async def sendmessage(self):
run_command = asyncio.create_task(tcp_echo_client("hey",self_loop))
asyncio.run(run_command)
if __name__ == '__main__':
app = QApplication(sys.argv)
loop = QEventLoop(app)
asyncio.set_event_loop(loop) # NEW must set the event loop sys.exit(app.exec_())
ex = PushButton(loop)
ex.show()
with loop:
loop.run_forever()
the simple echo client routine
import asyncio
async def tcp_echo_client(message, loop):
reader, writer = await asyncio.open_connection('127.0.0.1', 8889,
loop=loop)
print('Send: %r' % message)
writer.write(message.encode())
data = await reader.read(100)
print('Received: %r' % data.decode())
print('Close the socket')
writer.close()
and the responding server
import asyncio
async def handle_echo(reader, writer):
data = await reader.read(100)
message = data.decode()
addr = writer.get_extra_info('peername')
print("Received %r from %r" % (message, addr))
print("Send: %r" % message)
writer.write(data)
await writer.drain()
print("Close the client socket")
writer.close()
loop = asyncio.get_event_loop()
coro = asyncio.start_server(handle_echo, '127.0.0.1', 8889, loop=loop)
server = loop.run_until_complete(coro)
# Serve requests until Ctrl+C is pressed
print('Serving on {}'.format(server.sockets[0].getsockname()))
try:
loop.run_forever()
except KeyboardInterrupt:
pass
# Close the server
server.close()
loop.run_until_complete(server.wait_closed())
loop.close()
The problem is that if you invoke a slot that is a coroutine then you must use the asyncSlot decorator, also do not use ayncion.run() but await(In addition to eliminating other typos).
import sys
import asyncio
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton
from asyncqt import QEventLoop, asyncSlot
from async_client import tcp_echo_client
class PushButton(QWidget):
loop = None
def __init__(self, loop=None):
super(PushButton, self).__init__()
self.initUI()
self.loop = loop or asyncio.get_event_loop()
def initUI(self):
self.setWindowTitle("PushButton")
self.setGeometry(400, 400, 300, 260)
self.send_bttn = QPushButton(self)
self.send_bttn.setText("SEnd Message")
self.send_bttn.clicked.connect(self.sendmessage)
self.send_bttn.move(100, 100)
#asyncSlot()
async def sendmessage(self):
await tcp_echo_client("hey", self.loop)
if __name__ == "__main__":
app = QApplication(sys.argv)
loop = QEventLoop(app)
asyncio.set_event_loop(loop)
ex = PushButton(loop)
ex.show()
with loop:
loop.run_forever()

QProcess cannot read from a async Function of telethon

I am running a python file from a QProcess in a PyQt5 GUI.
I am showing everything from the terminal using readAllStandardOutput(I tried readAll also) to a textBox
In the python file, I am using telethon (library for using telegram API) python file is having a async event-handler in which I am printing something.
But before starting the starting the client readAllStandardOutput is reading fine. BUt after that print("Sent Hi!") is neither showing on the terminal nor Qprocess is reading it.
Pyqt5 GUI file having QProcess declaration.
class MainWindow(QMainWindow):
def __init__(self,parent = None):
super(QMainWindow, self).__init__(parent)
self.process = QProcess(self)
self.process.readyReadStandardOutput.connect(self.stdoutReady)
self.process.readyReadStandardError.connect(self.stderrReady)
self.process.started.connect(lambda: print('Started!'))
self.process.finished.connect(lambda: print('Finished!'))
self.process.setProcessChannelMode(QProcess.MergedChannels)
def startMain(self):
self.process.start('python3', ['mainApp2.py'])
def stdoutReady(self):
text = str(self.process.readAllStandardOutput())
self.mainTextBox.setText(str(text.strip()))
The python file QProess will start
import asyncio
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
print("Main App started")
api_id = #api_id integer
api_hash = "api_hash string"
client = TelegramClient('session', api_id, api_hash , loop = loop )
async def say_after(delay, what):
await asyncio.sleep(delay)
print(what)
#client.on(events.NewMessage)
async def my_event_handler(event):
if 'hello' in event.raw_text:
await event.reply('Hi!')
await say_after(1, 'Sent Hi!')
client.start()
client.run_until_disconnected()
"Main app Started" is displayed on the text box but not "Sent Hi!".
And the event.reply('Hi!') is working fine.
I am not sure may be it is because it is creating a child process
I want to redirect all output from the terminal to the textBox in the GUI (if you have other methods do suggest).

Categories