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