Skip to content
Snippets Groups Projects
Select Git revision
  • 6365dc0e69e98eb071ee3bf4905874e7b85b7fea
  • main default protected
  • release/1.1
  • encrypt_comments
  • mnemonic_dewif
  • authors_rules
  • 0.14
  • rtd
  • 1.2.1 protected
  • 1.2.0 protected
  • 1.1.1 protected
  • 1.1.0 protected
  • 1.0.0 protected
  • 1.0.0rc1 protected
  • 1.0.0rc0 protected
  • 1.0.0-rc protected
  • 0.62.0 protected
  • 0.61.0 protected
  • 0.60.1 protected
  • 0.58.1 protected
  • 0.60.0 protected
  • 0.58.0 protected
  • 0.57.0 protected
  • 0.56.0 protected
  • 0.55.1 protected
  • 0.55.0 protected
  • 0.54.3 protected
  • 0.54.2 protected
28 results

request_data_async.py

Blame
  • community.py 19.45 KiB
    '''
    Created on 1 févr. 2014
    
    @author: inso
    '''
    
    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.discover.network import Network
    from cutecoin.core.net.api import bma as qtbma
    import logging
    import inspect
    import hashlib
    import re
    import random
    import time
    import json
    from requests.exceptions import RequestException
    
    
    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.
    
            :param dict data: The cache in json format
            '''
            self.data = {}
            for entry in data['cache']:
                key = entry['key']
                cache_key = (key[0], key[1], key[2], key[3], key[4])
                self.data[cache_key] = entry['value']
    
            self.latest_block = data['latest_block']
    
        def jsonify(self):
            '''
            Get the cache in json format
    
            :return: The cache as a dict in json format
            '''
            data = {k: self.data[k] for k in self.data.keys()}
            entries = []
            for d in data:
                entries.append({'key': d,
                                'value': data[d]})
            return {'latest_block': self.latest_block,
                    'cache': entries}
    
        def refresh(self):
            '''
            Refreshing the cache just clears every last requests which
            cannot be saved because they can change from one block to another.
            '''
            logging.debug("Refresh : {0}/{1}".format(self.latest_block,
                                                     self.community.network.latest_block))
            if self.latest_block < self.community.network.latest_block:
                self.latest_block = self.community.network.latest_block
                self.data = {k: self.data[k] for k in self.data.keys()
                           if k[0] in Cache._saved_requests}
    
        def request(self, request, req_args={}, get_args={}):
            '''
            Send a cached request to a community.
            If the request was already sent in current block, return last value get.
    
            :param request: The request bma class
            :param req_args: The arguments passed to the request constructor
            :param get_args: The arguments passed to the requests __get__ method
            '''
            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())))))
    
            if cache_key not in self.data.keys():
                result = self.community.request(request, req_args, get_args,
                                             cached=False)
                # For block 0, we should have a different behaviour
                # Community members and certifications
                # Should be requested without caching
                self.data[cache_key] = result
                return self.data[cache_key]
            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] = {}
    
                if 'metadata' not in self.data[cache_key]:
                    self.data[cache_key]['metadata'] = {}
                self.data[cache_key]['metadata']['block'] = self.latest_block
    
                change = False
                if 'value' in self.data[cache_key]:
                    if self.data[cache_key]['value'] != json.loads(strdata):
                        change = True
                else:
                    change = True
    
                if change == True:
                    self.data[cache_key]['value'] = json.loads(strdata)
                    caller.data_changed.emit(request)
            else:
                logging.debug("Error in reply : {0}".format(reply.error()))
                self.community.qtrequest(caller, request, req_args, get_args)
    
    
    
    class Community(QObject):
        '''
        A community is a group of nodes using the same currency.
    
        .. 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(int)
    
        def __init__(self, currency, network):
            '''
            Initialize community attributes with a currency and a network.
    
            :param str currency: The currency name of the community.
            :param network: The network of the community
    
            .. warning:: The community object should be created using its factory
            class methods.
            '''
            super().__init__()
            self.currency = currency
            self._network = network
            self._cache = Cache(self)
            self._cache.refresh()
    
        @classmethod
        def create(cls, node):
            '''
            Create a community from its first node.
    
            :param node: The first Node of the community
            '''
            network = Network.create(node)
            community = cls(node.currency, network)
            logging.debug("Creating community")
            return community
    
        @classmethod
        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(network_manager, currency, json_data['peers'])
            community = cls(currency, network)
            return community
    
        def load_merge_network(self, json_data):
            '''
            Load the last state of the network.
            This network is merged with the network currently
            known network.
    
            :param dict json_data:
            '''
            self._network.merge_with_json(json_data)
    
        def load_cache(self, json_data):
            '''
            Load the community cache.
    
            :param dict json_data: The community as a dict in json format.
            '''
            self._cache.load_from_json(json_data)
    
        def jsonify_cache(self):
            '''
            Get the cache jsonified.
    
            :return: The cache as a dict in json format.
            '''
            return self._cache.jsonify()
    
        def jsonify_network(self):
            '''
            Get the network jsonified.
            '''
            return self._network.jsonify()
    
        @property
        def name(self):
            '''
            The community name is its currency name.
    
            :return: The community name
            '''
            return self.currency
    
        def __eq__(self, other):
            '''
            :return: True if this community has the same currency name as the other one.
            '''
            return (other.currency == self.currency)
    
        @property
        def short_currency(self):
            '''
            Format the currency name to a short one
    
            :return: The currency name in a shot format.
            '''
            words = re.split('[_\W]+', self.currency)
            shortened = ""
            if len(words) > 1:
                shortened = ''.join([w[0] for w in words])
            else:
                vowels = ('a', 'e', 'i', 'o', 'u', 'y')
                shortened = self.currency
                shortened = ''.join([c for c in shortened if c not in vowels])
            return shortened.upper()
    
        @property
        def currency_symbol(self):
            '''
            Format the currency name to a symbol one.
    
            :return: The currency name as a utf-8 circled symbol.
            '''
            letter = self.currency[0]
            u = ord('\u24B6') + ord(letter) - ord('A')
            return chr(u)
    
        @property
        def dividend(self):
            '''
            Get the last generated community universal dividend.
    
            :return: The last UD or 1 if no UD was generated.
            '''
            block = self.get_ud_block()
            if block:
                return block['dividend']
            else:
                return 1
    
        def get_ud_block(self, x=0):
            '''
            Get a block with universal dividend
    
            :param int x: Get the 'x' older block with UD in it
            :return: The last block with universal dividend.
            '''
            blocks = self.request(bma.blockchain.UD)['result']['blocks']
            if len(blocks) > 0:
                block_number = blocks[len(blocks)-(1+x)]
                block = self.request(bma.blockchain.Block,
                                     req_args={'number': block_number})
                return block
            else:
                return False
    
        @property
        def monetary_mass(self):
            '''
            Get the community monetary mass
    
            :return: The monetary mass value
            '''
            try:
                # Get cached block by block number
                block_number = self.network.latest_block
                block = self.request(bma.blockchain.Block,
                                     req_args={'number': block_number})
                return block['monetaryMass']
            except ValueError as e:
                if '404' in e:
                    return 0
            except NoPeerAvailable as e:
                return 0
    
        @property
        def nb_members(self):
            '''
            Get the community members number
    
            :return: The community members number
            '''
            try:
                # Get cached block by block number
                block_number = self.network.latest_block
                block = self.request(bma.blockchain.Block,
                                     req_args={'number': block_number})
                return block['membersCount']
            except ValueError as e:
                if '404' in e:
                    return 0
            except NoPeerAvailable as e:
                return 0
    
        @property
        def network(self):
            '''
            Get the community network instance.
    
            :return: The community network instance.
            '''
            return self._network
    
        def network_quality(self):
            '''
            Get a ratio of the synced nodes vs the rest
            '''
            synced = len(self._network.synced_nodes)
            #online = len(self._network.online_nodes)
            total = len(self._network.nodes)
            ratio_synced = synced / total
            return ratio_synced
    
        @property
        def parameters(self):
            '''
            Return community parameters in bma format
            '''
            return self.request(bma.blockchain.Parameters)
    
        def certification_expired(self, certtime):
            '''
            Return True if the certificaton time is too old
            '''
            return time.time() - certtime > self.parameters['sigValidity']
    
        def add_node(self, node):
            '''
            Add a peer to the community.
    
            :param peer: The new peer as a ucoinpy Peer object.
            '''
            self._network.add_root_node(node)
    
        def remove_node(self, index):
            '''
            Remove a node from the community.
    
            :param index: The index of the removed node.
            '''
            self._network.remove_root_node(index)
    
        def get_block(self, number=None):
            '''
            Get a block
    
            :param int number: The block number. If none, returns current block.
            '''
            if number is None:
                data = self.request(bma.blockchain.Current)
            else:
                logging.debug("Requesting block {0}".format(number))
                data = self.request(bma.blockchain.Block,
                                    req_args={'number': number})
    
            return Block.from_signed_raw("{0}{1}\n".format(data['raw'],
                                                           data['signature']))
    
        def current_blockid(self):
            '''
            Get the current block id.
    
            :return: The current block ID as [NUMBER-HASH] format.
            '''
            try:
                block = self.request(bma.blockchain.Current, cached=False)
                signed_raw = "{0}{1}\n".format(block['raw'], block['signature'])
                block_hash = hashlib.sha1(signed_raw.encode("ascii")).hexdigest().upper()
                block_number = block['number']
            except ValueError as e:
                if '404' in str(e):
                    block_number = 0
                    block_hash = "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709"
                else:
                    raise
            return {'number': block_number, 'hash': block_hash}
    
        def members_pubkeys(self):
            '''
            Listing members pubkeys of a community
    
            :return: All members pubkeys.
            '''
            memberships = self.qtrequest(self, qtbma.wot.Members)
            return [m['pubkey'] for m in memberships["results"]]
    
        def refresh_cache(self):
            '''
            Start the refresh processing of the cache
            '''
            self._cache.refresh()
    
        def request(self, request, req_args={}, get_args={}, cached=True):
            '''
            Start a request to the community.
    
            :param request: A ucoinpy bma request class
            :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:
                return self._cache.request(request, req_args, get_args)
            else:
                nodes = self._network.synced_nodes
                for node in nodes:
                    try:
                        req = request(node.endpoint.conn_handler(), **req_args)
                        data = req.get(**get_args)
    
                        if inspect.isgenerator(data):
                            generated = []
                            for d in data:
                                generated.append(d)
                            return generated
                        else:
                            return data
                    except ValueError as e:
                        if '502' in str(e):
                            continue
                        else:
                            raise
                    except RequestException as e:
                        logging.debug("Error : {1} : {0}".format(str(e),
                                                                 str(request)))
                        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 bma 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
                if len(nodes) > 0:
                    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
                else:
                    raise NoPeerAvailable(self.currency, len(nodes))
    
        def post(self, request, req_args={}, post_args={}):
            '''
            Post data to a community.
            Only sends the data to one node.
    
            :param request: A ucoinpy bma request class
            :param req_args: Arguments to pass to the request constructor
            :param post_args: Arguments to pass to the request __post__ method
            :return: The returned data
            '''
            nodes = self._network.online_nodes
            for node in nodes:
                req = request(node.endpoint.conn_handler(), **req_args)
                logging.debug("Trying to connect to : " + node.pubkey)
                req = request(node.endpoint.conn_handler(), **req_args)
                try:
                    req.post(**post_args)
                    return
                except ValueError as e:
                    raise
                except RequestException:
                    continue
            raise NoPeerAvailable(self.currency, len(nodes))
    
        def broadcast(self, request, req_args={}, post_args={}):
            '''
            Broadcast data to a community.
            Sends the data to all knew nodes.
    
            :param request: A ucoinpy bma request class
            :param req_args: Arguments to pass to the request constructor
            :param post_args: Arguments to pass to the request __post__ method
            :return: The returned data
    
            .. note:: If one node accept the requests (returns 200),
            the broadcast is considered accepted by the network.
            '''
            tries = 0
            ok = False
            value_error = None
            nodes = self._network.online_nodes
            for node in nodes:
                logging.debug("Trying to connect to : " + node.pubkey)
                req = request(node.endpoint.conn_handler(), **req_args)
                try:
                    req.post(**post_args)
                    ok = True
                except ValueError as e:
                    value_error = e
                    continue
                except RequestException:
                    tries = tries + 1
                    continue
    
            if not ok:
                raise value_error
    
            if tries == len(nodes):
                raise NoPeerAvailable(self.currency, len(nodes))
    
        def jsonify(self):
            '''
            Jsonify the community datas.
    
            :return: The community as a dict in json format.
            '''
    
            nodes_data = []
            for node in self._network.root_nodes:
                nodes_data.append(node.jsonify_root_node())
    
            data = {'currency': self.currency,
                    'peers': nodes_data}
            return data