From 5c6df81bdc79f182c71fceabf30a51a97bda0a6a Mon Sep 17 00:00:00 2001 From: Inso <insomniak.fr@gmail.com> Date: Sat, 20 Jun 2015 15:43:12 +0200 Subject: [PATCH] Using quamash, asyncio and coroutines Now, waiting for signals is way easier. --- src/cutecoin/core/__init__.py | 3 + src/cutecoin/core/account.py | 168 ++++++++++++++++------- src/cutecoin/core/app.py | 3 +- src/cutecoin/core/registry/identities.py | 1 + src/cutecoin/gui/certification.py | 49 ++++--- src/cutecoin/gui/community_tab.py | 47 ++----- src/cutecoin/main.py | 4 + 7 files changed, 165 insertions(+), 110 deletions(-) diff --git a/src/cutecoin/core/__init__.py b/src/cutecoin/core/__init__.py index e69de29b..cfb1a399 100644 --- a/src/cutecoin/core/__init__.py +++ b/src/cutecoin/core/__init__.py @@ -0,0 +1,3 @@ +from .community import Community +from .wallet import Wallet +from .account import Account \ No newline at end of file diff --git a/src/cutecoin/core/account.py b/src/cutecoin/core/account.py index 524c4de4..677caf55 100644 --- a/src/cutecoin/core/account.py +++ b/src/cutecoin/core/account.py @@ -12,6 +12,7 @@ import logging import time import math import json +import asyncio from PyQt5.QtCore import QObject, pyqtSignal, QCoreApplication, QT_TRANSLATE_NOOP from PyQt5.QtNetwork import QNetworkReply @@ -121,7 +122,9 @@ class Account(QObject): loading_progressed = pyqtSignal(int, int) inner_data_changed = pyqtSignal(str) wallets_changed = pyqtSignal() - document_broadcasted = pyqtSignal(str) + membership_broadcasted = pyqtSignal() + certification_broadcasted = pyqtSignal() + broadcast_error = pyqtSignal(int, str) def __init__(self, salt, pubkey, name, communities, wallets, contacts, identities_registry): ''' @@ -312,6 +315,7 @@ class Account(QObject): self.wallets = self.wallets[:size] self.wallets_changed.emit() + @asyncio.coroutine def certify(self, password, community, pubkey): """ Certify an other identity @@ -320,26 +324,76 @@ class Account(QObject): :param cutecoin.core.community.Community community: The community target of the certification :param str pubkey: The certified identity pubkey """ - certified = self._identities_registry.lookup(pubkey, community) - blockid = community.current_blockid() + blockid = "" + selfcert = None + + def build_certification_data(reply): + if reply.error() == QNetworkReply.NoError: + strdata = bytes(reply.readAll()).decode('utf-8') + json_data = json.loads(strdata) + nonlocal blockid + blockid = community.blockid(json_data) + future_certdata.set_result(True) + + def build_certification_selfcert(reply): + if reply.error() == QNetworkReply.NoError: + strdata = bytes(reply.readAll()).decode('utf-8') + json_data = json.loads(strdata) + nonlocal selfcert + selfcert = self._identities_registry.lookup(pubkey, community).selfcert(community, json_data) + future_selfcert.set_result(True) + + future_certdata = asyncio.Future() + reply = community.bma_access.request(qtbma.blockchain.Current) + reply.finished.connect(lambda: build_certification_data(reply)) + yield from future_certdata + + future_selfcert = asyncio.Future() + reply = community.bma_access.request( qtbma.wot.Lookup, req_args={'search': pubkey}) + reply.finished.connect(lambda: build_certification_selfcert(reply)) + yield from future_selfcert certification = Certification(PROTOCOL_VERSION, community.currency, - self.pubkey, certified.pubkey, + self.pubkey, pubkey, blockid['number'], blockid['hash'], None) - selfcert = certified.selfcert(community) - logging.debug("SelfCertification : {0}".format(selfcert.raw())) - key = SigningKey(self.salt, password) certification.sign(selfcert, [key]) signed_cert = certification.signed_raw(selfcert) logging.debug("Certification : {0}".format(signed_cert)) - data = {'pubkey': certified.pubkey, + data = {'pubkey': pubkey, 'self_': selfcert.signed_raw(), 'other': "{0}\n".format(certification.inline())} logging.debug("Posted data : {0}".format(data)) - community.broadcast(qtbma.wot.Add, {}, data) + replies = community.bma_access.broadcast(qtbma.wot.Add, {}, data) + for r in replies: + r.finished.connect(lambda reply=r: self.__handle_certification_reply(replies, reply)) + + def __handle_certification_reply(self, replies, reply): + """ + Handle the reply, if the request was accepted, disconnect + all other replies + + :param QNetworkReply reply: The reply of this handler + :param list of QNetworkReply replies: All request replies + :return: + """ + strdata = bytes(reply.readAll()).decode('utf-8') + logging.debug("Received reply : {0} : {1}".format(reply.error(), strdata)) + if reply.error() == QNetworkReply.NoError: + self.certification_broadcasted.emit() + for r in replies: + try: + r.disconnect() + except TypeError as e: + if "disconnect()" in str(e): + logging.debug("Could not disconnect a reply") + else: + for r in replies: + if not r.isFinished() or r.error() == QNetworkReply.NoError: + return + self.broadcast_error.emit(r.error(), strdata) def revoke(self, password, community): """ @@ -351,7 +405,6 @@ class Account(QObject): revoked = self._identities_registry.lookup(self.pubkey, community) revocation = Revocation(PROTOCOL_VERSION, community.currency, None) - selfcert = revoked.selfcert(community) key = SigningKey(self.salt, password) @@ -414,47 +467,63 @@ class Account(QObject): 'self_': selfcert.signed_raw(), 'other': []}) + @asyncio.coroutine def send_membership(self, password, community, mstype): ''' - Send a membership document to a target community + Send a membership document to a target community. + Signal "document_broadcasted" is emitted at the end. :param str password: The account SigningKey password :param community: The community target of the membership document :param str mstype: The type of membership demand. "IN" to join, "OUT" to leave ''' + blockid = "" + selfcert = None + logging.debug("Send membership") + + def build_membership_data(reply): + if reply.error() == QNetworkReply.NoError: + strdata = bytes(reply.readAll()).decode('utf-8') + json_data = json.loads(strdata) + nonlocal blockid + blockid = community.blockid(json_data) + future_msdata.set_result(True) + else: + raise ConnectionError(self.tr("Failed to get data build membership document")) + + def build_selfcert(reply): + if reply.error() == QNetworkReply.NoError: + strdata = bytes(reply.readAll()).decode('utf-8') + json_data = json.loads(strdata) + nonlocal selfcert + selfcert = self.identity(community).selfcert(community, json_data) + future_selfcert.set_result(True) + else: + raise ConnectionError(self.tr("Failed to get data build membership document")) + + future_msdata = asyncio.Future() reply = community.bma_access.request(qtbma.blockchain.Current) - reply.finished.connect(lambda: self.__build_membership_data(password, community, mstype, reply)) - - def __build_membership_data(self, password, community, mstype, reply): - if reply.error() == QNetworkReply.NoError: - strdata = bytes(reply.readAll()).decode('utf-8') - json_data = json.loads(strdata) - blockid = community.blockid(json_data) - reply = community.bma_access.request( qtbma.wot.Lookup, req_args={'search': self.pubkey}) - reply.finished.connect(lambda: self.__broadcast_membership(community, blockid, mstype, password, reply)) - else: - raise ConnectionError(self.tr("Failed to get data build membership document")) - - def __broadcast_membership(self, community, blockid, mstype, password, reply): - if reply.error() == QNetworkReply.NoError: - strdata = bytes(reply.readAll()).decode('utf-8') - json_data = json.loads(strdata) - selfcert = self.identity(community).selfcert(community, json_data) - membership = Membership(PROTOCOL_VERSION, community.currency, - selfcert.pubkey, blockid['number'], - blockid['hash'], mstype, selfcert.uid, - selfcert.timestamp, None) - key = SigningKey(self.salt, password) - membership.sign([key]) - logging.debug("Membership : {0}".format(membership.signed_raw())) - replies = community.bma_access.broadcast(qtbma.blockchain.Membership, {}, - {'membership': membership.signed_raw()}) - for r in replies: - r.finished.connect(lambda reply=r: self.__handle_broadcast_replies(replies, reply)) - else: - raise ConnectionError(self.tr("Failed to get data build membership document")) - - def __handle_broadcast_replies(self, replies, reply): + reply.finished.connect(lambda: build_membership_data(reply)) + logging.debug("msdata") + yield from future_msdata + future_selfcert = asyncio.Future() + reply = community.bma_access.request( qtbma.wot.Lookup, req_args={'search': self.pubkey}) + reply.finished.connect(lambda: build_selfcert(reply)) + logging.debug("selfcert") + yield from future_selfcert + membership = Membership(PROTOCOL_VERSION, community.currency, + selfcert.pubkey, blockid['number'], + blockid['hash'], mstype, selfcert.uid, + selfcert.timestamp, None) + key = SigningKey(self.salt, password) + membership.sign([key]) + logging.debug("Membership : {0}".format(membership.signed_raw())) + replies = community.bma_access.broadcast(qtbma.blockchain.Membership, {}, + {'membership': membership.signed_raw()}) + for r in replies: + r.finished.connect(lambda reply=r: self.__handle_membership_replies(replies, reply)) + + def __handle_membership_replies(self, replies, reply): """ Handle the reply, if the request was accepted, disconnect all other replies @@ -463,14 +532,21 @@ class Account(QObject): :param list of QNetworkReply replies: All request replies :return: """ + strdata = bytes(reply.readAll()).decode('utf-8') + logging.debug("Received reply : {0} : {1}".format(reply.error(), strdata)) if reply.error() == QNetworkReply.NoError: - self.document_broadcasted.emit("Membership") + self.membership_broadcasted.emit() for r in replies: try: r.disconnect() except TypeError as e: if "disconnect()" in str(e): logging.debug("Could not disconnect a reply") + else: + for r in replies: + if not r.isFinished() or r.error() == QNetworkReply.NoError: + return + self.broadcast_error.emit(r.error(), strdata) def jsonify(self): ''' @@ -493,7 +569,3 @@ class Account(QObject): 'wallets': data_wallets, 'contacts': self.contacts} return data - - def get_person(self): - return Person.from_metadata({'text': self.name, - 'id': self.pubkey}) diff --git a/src/cutecoin/core/app.py b/src/cutecoin/core/app.py index ea72eb68..0f10e73c 100644 --- a/src/cutecoin/core/app.py +++ b/src/cutecoin/core/app.py @@ -43,6 +43,7 @@ class Application(QObject): super().__init__() self.accounts = {} self.current_account = None + self.qapp = qapp self.available_version = (True, __version__, "") @@ -215,7 +216,7 @@ class Application(QObject): for wallet in account.wallets: wallet_path = os.path.join(config.parameters['home'], - account.name, '__cache__', wallet.pubkey) + account.name, '__cache__', wallet.pubkey + "_wal") if os.path.exists(wallet_path): with open(wallet_path, 'r') as json_data: data = json.load(json_data) diff --git a/src/cutecoin/core/registry/identities.py b/src/cutecoin/core/registry/identities.py index 79aa6ac6..a89663e7 100644 --- a/src/cutecoin/core/registry/identities.py +++ b/src/cutecoin/core/registry/identities.py @@ -53,6 +53,7 @@ class IdentitiesRegistry: :return: A new person if the pubkey was unknown or\ the known instance if pubkey was already known. + :rtype: cutecoin.core.registry.Identity """ if pubkey in self._instances: identity = self._instances[pubkey] diff --git a/src/cutecoin/gui/certification.py b/src/cutecoin/gui/certification.py index 96f2c5aa..77ddd6dc 100644 --- a/src/cutecoin/gui/certification.py +++ b/src/cutecoin/gui/certification.py @@ -4,8 +4,8 @@ Created on 24 dec. 2014 @author: inso ''' from PyQt5.QtWidgets import QDialog, QMessageBox, QDialogButtonBox, QApplication -from PyQt5.QtCore import Qt -from ..tools.exceptions import NoPeerAvailable +from PyQt5.QtCore import Qt, pyqtSlot +import quamash from ..gen_resources.certification_uic import Ui_CertificationDialog from . import toast @@ -16,12 +16,13 @@ class CertificationDialog(QDialog, Ui_CertificationDialog): classdocs ''' - def __init__(self, certifier, password_asker): + def __init__(self, certifier, app, password_asker): ''' Constructor ''' super().__init__() self.setupUi(self) + self.app = app self.account = certifier self.password_asker = password_asker self.community = self.account.communities[0] @@ -43,30 +44,19 @@ class CertificationDialog(QDialog, Ui_CertificationDialog): if password == "": return - try: - QApplication.setOverrideCursor(Qt.WaitCursor) - self.account.certify(password, self.community, pubkey) - toast.display(self.tr("Certification"), - self.tr("Success certifying {0} from {1}").format(pubkey, - self.community.currency)) - except ValueError as e: - QMessageBox.critical(self, self.tr("Certification"), - self.tr("Something wrong happened : {0}").format(e), - QMessageBox.Ok) - return - except NoPeerAvailable as e: - QMessageBox.critical(self, self.tr("Certification"), - self.tr("Couldn't connect to network : {0}").format(e), - QMessageBox.Ok) - return - except Exception as e: - QMessageBox.critical(self, self.tr("Error"), - "{0}".format(e), - QMessageBox.Ok) - return - finally: - QApplication.restoreOverrideCursor() + QApplication.setOverrideCursor(Qt.WaitCursor) + self.account.certification_broadcasted.connect(lambda: self.certification_sent(self.community, + pubkey)) + self.account.broadcast_error.connect(self.handle_error) + with quamash.QEventLoop(self.app.qapp) as loop: + loop.run_until_complete(self.account.certify(password, self.community, pubkey)) + + def certification_sent(self, pubkey, currency): + toast.display(self.tr("Certification"), + self.tr("Success certifying {0} from {1}").format(pubkey, currency)) + self.account.certification_broadcasted.disconnect() + QApplication.restoreOverrideCursor() super().accept() def change_current_community(self, index): @@ -78,6 +68,13 @@ class CertificationDialog(QDialog, Ui_CertificationDialog): self.button_box.button(QDialogButtonBox.Ok).setEnabled(False) self.button_box.button(QDialogButtonBox.Ok).setText(self.tr("Not a member")) + @pyqtSlot(int, str) + def handle_error(self, error_code, text): + toast.display(self.tr("Error"), self.tr("{0} : {1}".format(error_code, text))) + self.account.certification_broadcasted.disconnect() + self.account.broadcast_error.disconnect(self.handle_error) + QApplication.restoreOverrideCursor() + def recipient_mode_changed(self, pubkey_toggled): self.edit_pubkey.setEnabled(pubkey_toggled) self.combo_contact.setEnabled(not pubkey_toggled) diff --git a/src/cutecoin/gui/community_tab.py b/src/cutecoin/gui/community_tab.py index fe717efe..fb11b04e 100644 --- a/src/cutecoin/gui/community_tab.py +++ b/src/cutecoin/gui/community_tab.py @@ -17,6 +17,7 @@ from .wot_tab import WotTabWidget from .transfer import TransferMoneyDialog from .certification import CertificationDialog from . import toast +import quamash from ..tools.exceptions import LookupFailureError, NoPeerAvailable from ..core.registry import IdentitiesRegistry from ucoinpy.api import bma @@ -68,8 +69,7 @@ class CommunityTabWidget(QWidget, Ui_CommunityTabWidget): self.account.identity(self.community).inner_data_changed.connect(self.handle_account_identity_change) self.search_direct_connections() - self.account.document_broadcasted.connect(self.display_broadcast_toast) - + self.account.membership_broadcasted.connect(self.display_membership_toast) self.refresh_quality_buttons() def identity_context_menu(self, point): @@ -152,10 +152,10 @@ class CommunityTabWidget(QWidget, Ui_CommunityTabWidget): currency_tab = self.window().currencies_tabwidget.currentWidget() currency_tab.tab_history.table_history.model().sourceModel().refresh_transfers() - def certify_identity(self, person): - dialog = CertificationDialog(self.account, self.password_asker) + def certify_identity(self, identity): + dialog = CertificationDialog(self.account, self.app, self.password_asker) dialog.combo_community.setCurrentText(self.community.name) - dialog.edit_pubkey.setText(person.pubkey) + dialog.edit_pubkey.setText(identity.pubkey) dialog.radio_pubkey.setChecked(True) dialog.exec_() @@ -169,28 +169,16 @@ class CommunityTabWidget(QWidget, Ui_CommunityTabWidget): index_wot_tab = self.tabs_information.indexOf(self.wot_tab) self.tabs_information.setCurrentIndex(index_wot_tab) - @pyqtSlot(str) - def display_broadcast_toast(self, document): - toast.display(document, self.tr("Success sending {0} demand".format(document))) + @pyqtSlot() + def display_membership_toast(self): + toast.display(self.tr("Membership"), self.tr("Success sending Membership demand")) def send_membership_demand(self): password = self.password_asker.exec_() if self.password_asker.result() == QDialog.Rejected: return - - try: - self.account.send_membership(password, self.community, 'IN') - except ValueError as e: - QMessageBox.critical(self, self.tr("Join demand error"), - str(e)) - except LookupFailureError as e: - QMessageBox.critical(self, self.tr("Key not sent to community"), - self.tr(""""Your key wasn't sent in the community. -You can't request a membership.""")) - except NoPeerAvailable as e: - QMessageBox.critical(self, self.tr("Network error"), - self.tr("Couldn't connect to network : {0}").format(e), - QMessageBox.Ok) + with quamash.QEventLoop(self.app.qapp) as loop: + loop.run_until_complete(self.account.send_membership(password, self.community, 'IN')) # except Exception as e: # QMessageBox.critical(self, "Error", # "{0}".format(e), @@ -207,19 +195,8 @@ The process to join back the community later will have to be done again.""") if self.password_asker.result() == QDialog.Rejected: return - try: - self.account.send_membership(password, self.community, 'OUT') - except ValueError as e: - QMessageBox.critical(self, self.tr("Leaving demand error"), - str(e)) - except NoPeerAvailable as e: - QMessageBox.critical(self, self.tr("Network error"), - self.tr("Couldn't connect to network : {0}").format(e), - QMessageBox.Ok) - # except Exception as e: - # QMessageBox.critical(self, self.tr("Error"), - # "{0}".format(e), - # QMessageBox.Ok) + with quamash.QEventLoop(self.app.qapp) as loop: + loop.run_until_complete(self.account.send_membership(password, self.community, 'OUT')) def publish_uid(self): reply = QMessageBox.warning(self, self.tr("Warning"), diff --git a/src/cutecoin/main.py b/src/cutecoin/main.py index f1a102a1..4d5cab59 100755 --- a/src/cutecoin/main.py +++ b/src/cutecoin/main.py @@ -7,7 +7,9 @@ import signal import sys import os import logging +import asyncio +from quamash import QEventLoop from PyQt5.QtWidgets import QApplication from cutecoin.gui.mainwindow import MainWindow from cutecoin.core.app import Application @@ -18,6 +20,8 @@ if __name__ == '__main__': cutecoin = QApplication(sys.argv) app = Application(sys.argv, cutecoin) + loop = QEventLoop(app) + asyncio.set_event_loop(loop) window = MainWindow(app) window.showMaximized() sys.exit(cutecoin.exec_()) -- GitLab