diff --git a/src/cutecoin/core/account.py b/src/cutecoin/core/account.py index 4855ec67f5de1af42d804c33b4b56722fe4b62e6..9604c53f08c1e657f30167c0e59e57f5815deabb 100644 --- a/src/cutecoin/core/account.py +++ b/src/cutecoin/core/account.py @@ -107,7 +107,7 @@ class Account(QObject): return account @classmethod - def load(cls, json_data): + def load(cls, network_manager, json_data): ''' Factory method to create an Account object from its json view. :param dict json_data: The account view as a json dict @@ -128,7 +128,7 @@ class Account(QObject): communities = [] for data in json_data['communities']: - community = Community.load(data) + community = Community.load(network_manager, data) communities.append(community) account = cls(salt, pubkey, name, communities, wallets, diff --git a/src/cutecoin/core/app.py b/src/cutecoin/core/app.py index 9c43fbccdcc6e787de8e60619715054d07878381..6246660bc84600352e36ecfab3ba1d57ba02c61b 100644 --- a/src/cutecoin/core/app.py +++ b/src/cutecoin/core/app.py @@ -48,7 +48,6 @@ class Application(QObject): self.available_version = __version__ config.parse_arguments(argv) self._network_manager = QNetworkAccessManager() - self._network_manager.finished.connect(self.read_available_version) self.get_last_version() self.preferences = {'account': "", 'lang': 'en_GB', @@ -173,7 +172,7 @@ class Application(QObject): account_name, 'properties') with open(account_path, 'r') as json_data: data = json.load(json_data) - account = Account.load(data) + account = Account.load(self._network_manager, data) self.load_cache(account) self.accounts[account_name] = account @@ -396,10 +395,12 @@ class Application(QObject): def get_last_version(self): url = QUrl("https://api.github.com/repos/ucoin-io/cutecoin/releases") request = QNetworkRequest(url) - self._network_manager.get(request) + reply = self._network_manager.get(request) + reply.finished.connect(self.read_available_version) - @pyqtSlot(QNetworkReply) - def read_available_version(self, reply): + @pyqtSlot() + def read_available_version(self): + reply = self.sender() latest = None releases = reply.readAll().data().decode('utf-8') logging.debug(releases) diff --git a/src/cutecoin/core/community.py b/src/cutecoin/core/community.py index be5e70adf47c298e1c9ad62eafcad290fb0669ef..feb42d3fa4b94e3073db2fe82204f27623b4ed8e 100644 --- a/src/cutecoin/core/community.py +++ b/src/cutecoin/core/community.py @@ -4,31 +4,44 @@ Created on 1 févr. 2014 @author: inso ''' -from PyQt5.QtCore import QObject, pyqtSignal +from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot +from PyQt5.QtNetwork import QNetworkReply from ucoinpy.api import bma from ucoinpy.documents.block import Block from ..tools.exceptions import NoPeerAvailable -from .net.node import Node from .net.network import Network +from .net.api import qtbma import logging import inspect import hashlib import re +import random import time +import json from requests.exceptions import RequestException -class Cache(): +class Cache(QObject): _saved_requests = [str(bma.blockchain.Block), str(bma.blockchain.Parameters)] + _zero_values = {qtbma.wot.Members : { + "results": [ + ] +} + } def __init__(self, community): ''' Init an empty cache ''' + super().__init__() self.latest_block = 0 self.community = community self.data = {} + @property + def network(self): + return self.community.network + def load_from_json(self, data): ''' Put data in the cache from json datas. @@ -95,6 +108,63 @@ class Cache(): else: return self.data[cache_key] + def qtrequest(self, caller, request, req_args={}, get_args={}): + cache_key = (str(request), + str(tuple(frozenset(sorted(req_args.keys())))), + str(tuple(frozenset(sorted(req_args.values())))), + str(tuple(frozenset(sorted(get_args.keys())))), + str(tuple(frozenset(sorted(get_args.values()))))) + + ret_data = None + if cache_key in self.data.keys(): + need_reload = False + if 'metadata' in self.data[cache_key]: + if self.data[cache_key]['metadata']['block'] < self.latest_block: + need_reload = True + else: + need_reload = True + ret_data = self.data[cache_key]['value'] + else: + need_reload = True + ret_data = Cache._zero_values[request] + + if need_reload: + #Move to network nstead of community + #after removing qthreads + reply = self.community.qtrequest(caller ,request, req_args, get_args, + cached=False) + reply.finished.connect(lambda: + self.handle_reply(caller, request, req_args, get_args)) + + return ret_data + + @pyqtSlot(int, dict, dict, QObject) + def handle_reply(self, caller, request, req_args, get_args): + reply = self.sender() + logging.debug("Handling QtNetworkReply for {0}".format(str(request))) + if reply.error() == QNetworkReply.NoError: + cache_key = (str(request), + str(tuple(frozenset(sorted(req_args.keys())))), + str(tuple(frozenset(sorted(req_args.values())))), + str(tuple(frozenset(sorted(get_args.keys())))), + str(tuple(frozenset(sorted(get_args.values()))))) + strdata = bytes(reply.readAll()).decode('utf-8') + logging.debug("Data in reply : {0}".format(strdata)) + + if cache_key not in self.data: + self.data[cache_key] = {} + self.data[cache_key]['value'] = json.loads(strdata) + + if 'metadata' not in self.data[cache_key]: + self.data[cache_key]['metadata'] = {} + self.data[cache_key]['metadata']['block'] = self.latest_block + + caller.data_changed.emit() + else: + logging.debug("Error in reply : {0}".format(reply.error())) + self.community.qtrequest(caller, request, req_args, get_args) + + class Community(QObject): ''' @@ -103,6 +173,7 @@ class Community(QObject): .. warning:: The currency name is supposed to be unique in cutecoin but nothing exists in ucoin to assert that a currency name is unique. ''' + data_changed = pyqtSignal() def __init__(self, currency, network): ''' @@ -133,14 +204,14 @@ class Community(QObject): return community @classmethod - def load(cls, json_data): + def load(cls, network_manager, json_data): ''' Load a community from json :param dict json_data: The community as a dict in json format ''' currency = json_data['currency'] - network = Network.from_json(currency, json_data['peers']) + network = Network.from_json(network_manager, currency, json_data['peers']) community = cls(currency, network) return community @@ -375,7 +446,7 @@ class Community(QObject): :return: All members pubkeys. ''' - memberships = self.request(bma.wot.Members) + memberships = self.qtrequest(self, qtbma.wot.Members) return [m['pubkey'] for m in memberships["results"]] def refresh_cache(self): @@ -420,6 +491,31 @@ class Community(QObject): continue raise NoPeerAvailable(self.currency, len(nodes)) + def qtrequest(self, caller, request, req_args={}, get_args={}, cached=True): + ''' + Start a request to the community. + + :param request: A qtbma request class calling for data + :param caller: The components + :param req_args: Arguments to pass to the request constructor + :param get_args: Arguments to pass to the request __get__ method + :return: The returned data if cached = True else return the QNetworkReply + ''' + if cached: + return self._cache.qtrequest(caller, request, req_args, get_args) + else: + nodes = self._network.synced_nodes + node = random.choice(nodes) + server = node.endpoint.conn_handler().server + port = node.endpoint.conn_handler().port + conn_handler = qtbma.ConnectionHandler(self.network.network_manager, server, port) + req = request(conn_handler, **req_args) + reply = req.get(**get_args) + return reply + + if len(self.network.synced_nodes) == 0: + raise NoPeerAvailable(self.currency, len(nodes)) + def post(self, request, req_args={}, post_args={}): ''' Post data to a community. diff --git a/src/cutecoin/core/net/api/bma/__init__.py b/src/cutecoin/core/net/api/qtbma/__init__.py similarity index 79% rename from src/cutecoin/core/net/api/bma/__init__.py rename to src/cutecoin/core/net/api/qtbma/__init__.py index e2ff659d9e4d35c29b50525d17951d0368d39f90..145eb7c8a82b34cedb4e19c1990283ff370876b7 100644 --- a/src/cutecoin/core/net/api/bma/__init__.py +++ b/src/cutecoin/core/net/api/qtbma/__init__.py @@ -1,3 +1,5 @@ + + __all__ = ['api'] from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest @@ -11,13 +13,14 @@ logger = logging.getLogger("ucoin") class ConnectionHandler(object): """Helper class used by other API classes to ease passing server connection information.""" - def __init__(self, server, port): + def __init__(self, network_manager, server, port): """ Arguments: - `server`: server hostname - `port`: port number """ + self.network_manager = network_manager self.server = server self.port = port @@ -28,7 +31,7 @@ class ConnectionHandler(object): class API(object): """APIRequest is a class used as an interface. The intermediate derivated classes are the modules and the leaf classes are the API requests.""" - def __init__(self, network_manager, connection_handler, module): + def __init__(self, conn_handler, module): """ Asks a module in order to create the url used then by derivated classes. @@ -38,8 +41,7 @@ class API(object): """ self.module = module - self.network_manager = network_manager - self.connection_handler = connection_handler + self.conn_handler = conn_handler self.headers = {} def reverse_url(self, path): @@ -50,7 +52,7 @@ class API(object): - `path`: the request path """ - server, port = self.connection_handler.server, self.connection_handler.port + server, port = self.conn_handler.server, self.conn_handler.port url = 'http://%s:%d/%s' % (server, port, self.module) return url + path @@ -88,12 +90,13 @@ class API(object): Arguments: - `path`: the request path """ - url = QUrlQuery(self.reverse_url(path)) + query = QUrlQuery(self.reverse_url(path)) for k,v in kwargs.items(): - url.addQueryItem(k, v); + query.addQueryItem(k, v); + url = QUrl(self.reverse_url(path)) + url.setQuery(query) request = QNetworkRequest(url) - reply = request.get(self.reverse_url(path), params=kwargs, - headers=self.headers, timeout=15) + reply = self.conn_handler.network_manager.get(request) return reply @@ -110,12 +113,14 @@ class API(object): logging.debug("POST : {0}".format(kwargs)) post_data = QUrlQuery() for k,v in kwargs.items(): - post_data.addQueryItem(k, v); + post_data.addQueryItem(k, v) + url = QUrl(self.reverse_url(path)) + url.setQuery(post_data) - request = QNetworkRequest(self.reverse_url(path)) + request = QNetworkRequest(url) request.setHeader(QNetworkRequest.ContentTypeHeader, - "application/x-www-form-urlencoded"); - reply = request.post(self.reverse_url(path), + "application/x-www-form-urlencoded") + reply = self.conn_handler.network_manager.post(request, post_data.toString(QUrl.FullyEncoded).toUtf8()) return reply diff --git a/src/cutecoin/core/net/api/bma/blockchain/__init__.py b/src/cutecoin/core/net/api/qtbma/blockchain/__init__.py similarity index 89% rename from src/cutecoin/core/net/api/bma/blockchain/__init__.py rename to src/cutecoin/core/net/api/qtbma/blockchain/__init__.py index 1766de673e40304c3ae6558fbefd2a71c67c98b3..b603bc0238cc88cda96bebdde24a6287e6c43442 100644 --- a/src/cutecoin/core/net/api/bma/blockchain/__init__.py +++ b/src/cutecoin/core/net/api/qtbma/blockchain/__init__.py @@ -22,8 +22,8 @@ logger = logging.getLogger("ucoin/blockchain") class Blockchain(API): - def __init__(self, connection_handler, module='blockchain'): - super(Blockchain, self).__init__(connection_handler, module) + def __init__(self, conn_handler, module='blockchain'): + super(Blockchain, self).__init__(conn_handler, module) class Parameters(Blockchain): @@ -35,8 +35,8 @@ class Parameters(Blockchain): class Membership(Blockchain): """GET/POST a Membership document.""" - def __init__(self, connection_handler, search=None): - super().__init__(connection_handler) + def __init__(self, conn_handler, search=None): + super().__init__(conn_handler) self.search = search def __post__(self, **kwargs): @@ -52,7 +52,7 @@ class Membership(Blockchain): class Block(Blockchain): """GET/POST a block from/to the blockchain.""" - def __init__(self, connection_handler, number=None): + def __init__(self, conn_handler, number=None): """ Use the number parameter in order to select a block number. @@ -60,7 +60,7 @@ class Block(Blockchain): - `number`: block number to select """ - super(Block, self).__init__(connection_handler) + super(Block, self).__init__(conn_handler) self.number = number @@ -85,7 +85,7 @@ class Current(Blockchain): class Hardship(Blockchain): """GET hardship level for given member's fingerprint for writing next block.""" - def __init__(self, connection_handler, fingerprint): + def __init__(self, conn_handler, fingerprint): """ Use the number parameter in order to select a block number. @@ -93,7 +93,7 @@ class Hardship(Blockchain): - `fingerprint`: member fingerprint """ - super(Hardship, self).__init__(connection_handler) + super(Hardship, self).__init__(conn_handler) self.fingerprint = fingerprint diff --git a/src/cutecoin/core/net/api/bma/network/__init__.py b/src/cutecoin/core/net/api/qtbma/network/__init__.py similarity index 88% rename from src/cutecoin/core/net/api/bma/network/__init__.py rename to src/cutecoin/core/net/api/qtbma/network/__init__.py index 21d2735eb0e9a6a90d52d1febce86f0a7d11a38e..252219946af9fd6471dd6f20bf3f95659173d19c 100644 --- a/src/cutecoin/core/net/api/bma/network/__init__.py +++ b/src/cutecoin/core/net/api/qtbma/network/__init__.py @@ -22,8 +22,8 @@ logger = logging.getLogger("ucoin/network") class Network(API): - def __init__(self, connection_handler, module='network'): - super(Network, self).__init__(connection_handler, module) + def __init__(self, conn_handler, module='network'): + super(Network, self).__init__(conn_handler, module) class Peering(Network): diff --git a/src/cutecoin/core/net/api/bma/network/peering/__init__.py b/src/cutecoin/core/net/api/qtbma/network/peering/__init__.py similarity index 92% rename from src/cutecoin/core/net/api/bma/network/peering/__init__.py rename to src/cutecoin/core/net/api/qtbma/network/peering/__init__.py index b31c63b3f80315fb81d20e35f735200c0c6cf68a..1127f275263d76873e49f4d923b2b8752f2e40ff 100644 --- a/src/cutecoin/core/net/api/bma/network/peering/__init__.py +++ b/src/cutecoin/core/net/api/qtbma/network/peering/__init__.py @@ -22,8 +22,8 @@ logger = logging.getLogger("ucoin/network/peering") class Base(Network): - def __init__(self, connection_handler): - super(Base, self).__init__(connection_handler, 'network/peering') + def __init__(self, conn_handler): + super(Base, self).__init__(conn_handler, 'network/peering') class Peers(Base): diff --git a/src/cutecoin/core/net/api/bma/tx/__init__.py b/src/cutecoin/core/net/api/qtbma/tx/__init__.py similarity index 83% rename from src/cutecoin/core/net/api/bma/tx/__init__.py rename to src/cutecoin/core/net/api/qtbma/tx/__init__.py index 6c670fa6b721433d5eb7983963c0199d01bbafb3..94dab6583217aed0af39ea0a235829ce6c4c9c4e 100644 --- a/src/cutecoin/core/net/api/bma/tx/__init__.py +++ b/src/cutecoin/core/net/api/qtbma/tx/__init__.py @@ -22,8 +22,8 @@ logger = logging.getLogger("ucoin/tx") class Tx(API): - def __init__(self, connection_handler, module='tx'): - super(Tx, self).__init__(connection_handler, module) + def __init__(self, conn_handler, module='tx'): + super(Tx, self).__init__(conn_handler, module) class Process(Tx): @@ -37,8 +37,8 @@ class Process(Tx): class Sources(Tx): """Get transaction sources.""" - def __init__(self, connection_handler, pubkey, module='tx'): - super(Tx, self).__init__(connection_handler, module) + def __init__(self, conn_handler, pubkey, module='tx'): + super(Tx, self).__init__(conn_handler, module) self.pubkey = pubkey def __get__(self, **kwargs): diff --git a/src/cutecoin/core/net/api/bma/wot/__init__.py b/src/cutecoin/core/net/api/qtbma/wot/__init__.py similarity index 75% rename from src/cutecoin/core/net/api/bma/wot/__init__.py rename to src/cutecoin/core/net/api/qtbma/wot/__init__.py index 2da4a52582fcc093e3b2db7257e5f68777fc6875..264dd5aa4c839cd262392d215ee605f5185dc3eb 100644 --- a/src/cutecoin/core/net/api/bma/wot/__init__.py +++ b/src/cutecoin/core/net/api/qtbma/wot/__init__.py @@ -22,8 +22,8 @@ logger = logging.getLogger("ucoin/wot") class WOT(API): - def __init__(self, connection_handler, module='wot'): - super(WOT, self).__init__(connection_handler, module) + def __init__(self, conn_handler, module='wot'): + super(WOT, self).__init__(conn_handler, module) class Add(WOT): @@ -40,8 +40,8 @@ class Add(WOT): class Lookup(WOT): """GET Public key data.""" - def __init__(self, connection_handler, search, module='wot'): - super(WOT, self).__init__(connection_handler, module) + def __init__(self, conn_handler, search, module='wot'): + super(WOT, self).__init__(conn_handler, module) self.search = search @@ -54,8 +54,8 @@ class Lookup(WOT): class CertifiersOf(WOT): """GET Certification data over a member.""" - def __init__(self, connection_handler, search, module='wot'): - super(WOT, self).__init__(connection_handler, module) + def __init__(self, conn_handler, search, module='wot'): + super(WOT, self).__init__(conn_handler, module) self.search = search @@ -68,8 +68,8 @@ class CertifiersOf(WOT): class CertifiedBy(WOT): """GET Certification data from a member.""" - def __init__(self, connection_handler, search, module='wot'): - super(WOT, self).__init__(connection_handler, module) + def __init__(self, conn_handler, search, module='wot'): + super(WOT, self).__init__(conn_handler, module) self.search = search @@ -82,8 +82,8 @@ class CertifiedBy(WOT): class Members(WOT): """GET List all current members of the Web of Trust.""" - def __init__(self, connection_handler, module='wot'): - super(WOT, self).__init__(connection_handler, module) + def __init__(self, conn_handler, module='wot'): + super(WOT, self).__init__(conn_handler, module) def __get__(self, **kwargs): return self.requests_get('/members', **kwargs) diff --git a/src/cutecoin/core/net/network.py b/src/cutecoin/core/net/network.py index 34f533ccc91e5567ce64db14b82aa7903f44a319..901cc30bfe541e616afa867307dfccc75c179b4e 100644 --- a/src/cutecoin/core/net/network.py +++ b/src/cutecoin/core/net/network.py @@ -21,7 +21,7 @@ class Network(Watcher): new_block_mined = pyqtSignal(int) stopped_perpetual_crawling = pyqtSignal() - def __init__(self, currency, nodes): + def __init__(self, network_manager, currency, nodes): ''' Constructor of a network @@ -36,10 +36,11 @@ class Network(Watcher): self.nodes = nodes self._must_crawl = False self._is_perpetual = False + self.network_manager = network_manager self._block_found = self.latest_block @classmethod - def create(cls, node): + def create(cls, network_manager, node): ''' Create a new network with one knew node Crawls the nodes from the first node to build the @@ -48,7 +49,7 @@ class Network(Watcher): :param node: The first knew node of the network ''' nodes = [node] - network = cls(node.currency, nodes) + network = cls(network_manager, node.currency, nodes) return network def merge_with_json(self, json_data): @@ -59,7 +60,7 @@ class Network(Watcher): :param dict json_data: Nodes in json format ''' for data in json_data: - node = Node.from_json(self.currency, data) + node = Node.from_json(self.network_manager, self.currency, data) if node.pubkey not in [n.pubkey for n in self.nodes]: self.add_node(node) logging.debug("Loading : {:}".format(data['pubkey'])) @@ -71,7 +72,7 @@ class Network(Watcher): other_node.state = node.state @classmethod - def from_json(cls, currency, json_data): + def from_json(cls, network_manager, currency, json_data): ''' Load a network from a configured community @@ -80,9 +81,9 @@ class Network(Watcher): ''' nodes = [] for data in json_data: - node = Node.from_json(currency, data) + node = Node.from_json(network_manager, currency, data) nodes.append(node) - network = cls(currency, nodes) + network = cls(network_manager, currency, nodes) return network def jsonify(self): diff --git a/src/cutecoin/core/net/node.py b/src/cutecoin/core/net/node.py index 8f2be8009e6b215a707d403a9ae6b48cee438309..bc4c5809baeff02cb0529f33981936fa95e86dc8 100644 --- a/src/cutecoin/core/net/node.py +++ b/src/cutecoin/core/net/node.py @@ -35,12 +35,13 @@ class Node(QObject): changed = pyqtSignal() - def __init__(self, currency, endpoints, uid, pubkey, block, + def __init__(self, network_manager, currency, endpoints, uid, pubkey, block, state, last_change): ''' Constructor ''' super().__init__() + self.network_manager = network_manager self._endpoints = endpoints self._uid = uid self._pubkey = pubkey @@ -51,7 +52,7 @@ class Node(QObject): self._last_change = last_change @classmethod - def from_address(cls, currency, address, port): + def from_address(cls, network_manager, currency, address, port): ''' Factory method to get a node from a given address @@ -69,13 +70,14 @@ class Node(QObject): if peer.currency != currency: raise InvalidNodeCurrency(peer.currency, currency) - node = cls(peer.currency, peer.endpoints, "", peer.pubkey, 0, + node = cls(network_manager, peer.currency, peer.endpoints, + "", peer.pubkey, 0, Node.ONLINE, time.time()) logging.debug("Node from address : {:}".format(str(node))) return node @classmethod - def from_peer(cls, currency, peer): + def from_peer(cls, network_manager, currency, peer): ''' Factory method to get a node from a peer document. @@ -87,13 +89,13 @@ class Node(QObject): if peer.currency != currency: raise InvalidNodeCurrency(peer.currency, currency) - node = cls(peer.currency, peer.endpoints, "", "", 0, + node = cls(network_manager, peer.currency, peer.endpoints, "", "", 0, Node.ONLINE, time.time()) logging.debug("Node from peer : {:}".format(str(node))) return node @classmethod - def from_json(cls, currency, data): + def from_json(cls, network_manager, currency, data): endpoints = [] uid = "" pubkey = "" @@ -124,7 +126,8 @@ class Node(QObject): else: logging.debug("Error : no state in node") - node = cls(currency, endpoints, uid, pubkey, block, + node = cls(network_manager, currency, endpoints, + uid, pubkey, block, state, last_change) logging.debug("Node from json : {:}".format(str(node))) return node diff --git a/src/cutecoin/gui/community_tab.py b/src/cutecoin/gui/community_tab.py index df57be28753f8890fdd5629ee70ca3fb05ea38a2..ff0e7938a082789f7f1719508ff3aa790299c251 100644 --- a/src/cutecoin/gui/community_tab.py +++ b/src/cutecoin/gui/community_tab.py @@ -42,7 +42,9 @@ class CommunityTabWidget(QWidget, Ui_CommunityTabWidget): self.setupUi(self) self.parent = parent self.community = community + self.community.data_changed.connect(self.handle_change) self.account = account + self._last_search = '' self.password_asker = password_asker identities_model = IdentitiesTableModel(community) proxy = IdentitiesFilterProxyModel() @@ -259,9 +261,14 @@ Publishing your UID cannot be canceled.""") for identity in response['results']: persons.append(Person.lookup(identity['pubkey'], self.community)) + self._last_search = 'text' self.edit_textsearch.clear() self.refresh(persons) + def handle_change(self): + if self._last_search == 'members': + self.search_members() + def search_members(self): """ Search members of community and display found members @@ -271,6 +278,8 @@ Publishing your UID cannot be canceled.""") for p in pubkeys: persons.append(Person.lookup(p, self.community)) + self._last_search = 'members' + self.edit_textsearch.clear() self.refresh(persons) @@ -278,6 +287,7 @@ Publishing your UID cannot be canceled.""") """ Search members of community and display found members """ + self._last_search = 'direct_connections' self.refresh() def refresh(self, persons=None):