''' Created on 11 févr. 2014 @author: inso ''' import logging import functools from ucoinpy.api import bma from ucoinpy import PROTOCOL_VERSION from ucoinpy.documents.certification import SelfCertification from ucoinpy.documents.membership import Membership from ..tools.exceptions import Error, PersonNotFoundError,\ MembershipNotFoundError from PyQt5.QtCore import QMutex def load_cache(json_data): for person_data in json_data['persons']: person = Person.from_json(person_data) Person._instances[person.pubkey] = person def jsonify_cache(): data = [] for person in Person._instances.values(): data.append(person.jsonify()) return {'persons': data} class cached(object): ''' Decorator. Caches a function's return value each time it is called. If called later with the same arguments, the cached value is returned (not reevaluated). Delete it to clear it from the cache ''' def __init__(self, func): self.func = func def __call__(self, inst, community): inst._cache_mutex.lock() try: inst._cache[community.currency] except KeyError: inst._cache[community.currency] = {} try: value = inst._cache[community.currency][self.func.__name__] except KeyError: value = self.func(inst, community) inst._cache[community.currency][self.func.__name__] = value finally: inst._cache_mutex.unlock() inst._cache_mutex.unlock() return value def __repr__(self): '''Return the function's docstring.''' return self.func.__repr__ def __get__(self, inst, objtype): if inst is None: return self.func return functools.partial(self, inst) #TODO: Change Person to Identity ? class Person(object): ''' A person with a uid and a pubkey ''' _instances = {} def __init__(self, uid, pubkey, cache): ''' Initializing a person object. :param str uid: The person uid, also known as its uid on the network :param str pubkey: The person pubkey :param cache: The last returned values of the person properties. ''' super().__init__() self.uid = uid self.pubkey = pubkey self._cache = cache self._cache_mutex = QMutex() @classmethod def lookup(cls, pubkey, community, cached=True): ''' Get a person from the pubkey found in a community :param str pubkey: The person pubkey :param community: The community in which to look for the pubkey :param bool cached: True if the person should be searched in the cache before requesting the community. :return: A new person if the pubkey was unknown or\ the known instance if pubkey was already known. ''' if cached and pubkey in Person._instances: return Person._instances[pubkey] else: try: data = community.request(bma.wot.Lookup, req_args={'search': pubkey}, cached=cached) except ValueError as e: if '404' in str(e): raise PersonNotFoundError(pubkey, community.name) timestamp = 0 for result in data['results']: if result["pubkey"] == pubkey: uids = result['uids'] for uid in uids: if uid["meta"]["timestamp"] > timestamp: timestamp = uid["meta"]["timestamp"] uid = uid["uid"] person = cls(uid, pubkey, {}) Person._instances[pubkey] = person logging.debug("{0}".format(Person._instances.keys())) return person raise PersonNotFoundError(pubkey, community.name) @classmethod def from_metadata(cls, metadata): ''' Get a person from a metadata dict. A metadata dict has a 'text' key corresponding to the person uid, and a 'id' key corresponding to the person pubkey. :param dict metadata: The person metadata :return: A new person if pubkey wasn't knwon, else the existing instance. ''' uid = metadata['text'] pubkey = metadata['id'] if pubkey in Person._instances: return Person._instances[pubkey] else: person = cls(uid, pubkey, {}) Person._instances[pubkey] = person return person @classmethod def from_json(cls, json_data): ''' Create a person from json data :param dict json_data: The person as a dict in json format :return: A new person if pubkey wasn't known, else a new person instance. ''' pubkey = json_data['pubkey'] if pubkey in Person._instances: return Person._instances[pubkey] else: if 'name' in json_data: uid = json_data['name'] else: uid = json_data['uid'] if 'cache' in json_data: cache = json_data['cache'] else: cache = {} person = cls(uid, pubkey, cache) Person._instances[pubkey] = person return person def selfcert(self, community): ''' Get the person self certification. This request is not cached in the person object. :param community: The community target to request the self certification :return: A SelfCertification ucoinpy object ''' data = community.request(bma.wot.Lookup, req_args={'search': self.pubkey}) logging.debug(data) timestamp = 0 for result in data['results']: if result["pubkey"] == self.pubkey: uids = result['uids'] for uid_data in uids: if uid_data["meta"]["timestamp"] > timestamp: timestamp = uid_data["meta"]["timestamp"] uid = uid_data["uid"] signature = uid_data["self"] return SelfCertification(PROTOCOL_VERSION, community.currency, self.pubkey, timestamp, uid, signature) raise PersonNotFoundError(self.pubkey, community.name) @cached def get_join_date(self, community): ''' Get the person join date. This request is not cached in the person object. :param community: The community target to request the join date :return: A datetime object ''' try: search = community.request(bma.blockchain.Membership, {'search': self.pubkey}) membership_data = None if len(search['memberships']) > 0: membership_data = search['memberships'][0] return community.get_block(membership_data['blockNumber']).mediantime else: return None except ValueError as e: if '400' in str(e): raise MembershipNotFoundError(self.pubkey, community.name) #TODO: Manage 'OUT' memberships ? Maybe ? @cached def membership(self, community): ''' Get the person last membership document. :param community: The community target to request the join date :return: The membership data in BMA json format ''' try: search = community.request(bma.blockchain.Membership, {'search': self.pubkey}) block_number = -1 for ms in search['memberships']: if ms['blockNumber'] > block_number: block_number = ms['blockNumber'] if 'type' in ms: if ms['type'] is 'IN': membership_data = ms else: membership_data = ms if membership_data is None: raise MembershipNotFoundError(self.pubkey, community.name) except ValueError as e: if '400' in str(e): raise MembershipNotFoundError(self.pubkey, community.name) return membership_data @cached def is_member(self, community): ''' Check if the person is a member of a community :param community: The community target to request the join date :return: True if the person is a member of a community ''' try: certifiers = community.request(bma.wot.CertifiersOf, {'search': self.pubkey}) return certifiers['isMember'] except ValueError: return False @cached def certifiers_of(self, community): ''' Get the list of this person certifiers :param community: The community target to request the join date :return: The list of the certifiers of this community in BMA json format ''' try: certifiers = community.request(bma.wot.CertifiersOf, {'search': self.pubkey}) except ValueError as e: logging.debug('bma.wot.CertifiersOf request ValueError : ' + str(e)) try: data = community.request(bma.wot.Lookup, {'search': self.pubkey}) except ValueError as e: logging.debug('bma.wot.Lookup request ValueError : ' + str(e)) return list() # convert api data to certifiers list certifiers = list() # add certifiers of uid for certifier in data['results'][0]['uids'][0]['others']: # for each uid found for this pubkey... for uid in certifier['uids']: # add a certifier certifier['uid'] = uid certifier['cert_time'] = dict() certifier['cert_time']['medianTime'] = community.get_block(certifier['meta']['block_number']).mediantime certifiers.append(certifier) return certifiers except Exception as e: logging.debug('bma.wot.CertifiersOf request error : ' + str(e)) return list() return certifiers['certifications'] @cached def certified_by(self, community): ''' Get the list of persons certified by this person :param community: The community target to request the join date :return: The list of the certified persons of this community in BMA json format ''' try: certified_list = community.request(bma.wot.CertifiedBy, {'search': self.pubkey}) except ValueError as e: logging.debug('bma.wot.CertifiersOf request ValueError : ' + str(e)) try: data = community.request(bma.wot.Lookup, {'search': self.pubkey}) except ValueError as e: logging.debug('bma.wot.Lookup request ValueError : ' + str(e)) return list() certified_list = list() for certified in data['results'][0]['signed']: certified['cert_time'] = dict() certified['cert_time']['medianTime'] = certified['meta']['timestamp'] certified_list.append(certified) return certified_list except Exception as e: logging.debug('bma.wot.CertifiersOf request error : ' + str(e)) return list() return certified_list['certifications'] def reload(self, func, community): ''' Reload a cached property of this person in a community. This method is thread safe. This method clears the cache entry for this community and get it back. :param func: The cached property to reload :param community: The community to request for data :return: True if a changed was made by the reload. ''' self._cache_mutex.lock() try: if community.currency not in self._cache: self._cache[community.currency] = {} change = False before = self._cache[community.currency][func.__name__] value = func(self, community) if not change: if type(value) is dict: hash_before = (hash(tuple(frozenset(sorted(before.keys())))), hash(tuple(frozenset(sorted(before.items()))))) hash_after = (hash(tuple(frozenset(sorted(value.keys())))), hash(tuple(frozenset(sorted(value.items()))))) change = hash_before != hash_after elif type(value) is bool: change = before != value self._cache[community.currency][func.__name__] = value except KeyError: change = True except Error: return False finally: self._cache_mutex.unlock() return change def jsonify(self): ''' Get the community as dict in json format. :return: The community as a dict in json format ''' data = {'uid': self.uid, 'pubkey': self.pubkey, 'cache': self._cache} return data