[docs]defdisconnect_least_efficient(self):# type: (ChordSocket) -> bool"""Disconnects the node which provides the least value. This is determined by finding the node which is the closest to its neighbors, using the modulus distance metric Returns: A :py:class:`bool` that describes whether a node was disconnected """@inherit_doc(ChordConnection.id_10)defget_id(o):# type: (ChordConnection) -> intreturno.id_10defsmallest_gap(lst):# type: (Iterator[ChordConnection]) -> ChordConnectioncoll=sorted(lst,key=get_id)coll_len=len(coll)circular_triplets=((coll[x],coll[(x+1)%coll_len],coll[(x+2)%coll_len])forxinrange(coll_len))narrowest=None# type: Union[None, ChordConnection]gap=2**384# type: intforbeg,mid,endincircular_triplets:ifdistance(beg.id_10,end.id_10)<gapandmid.outgoing:gap=distance(beg.id_10,end.id_10)narrowest=midreturnnarrowestrelevant_nodes=(nodefornodeinself.data_storingifnotnode.leeching)to_kill=smallest_gap(relevant_nodes)ifto_kill:self.disconnect(to_kill)returnTruereturnFalse

[docs]def_handle_peers(self,msg,handler):# type: (ChordSocket, Message, BaseConnection) -> Union[bool, None]"""This callback is used to deal with peer signals. Its primary jobs is to connect to the given peers, if this does not exceed :py:const:`py2p.chord.max_outgoing` Args: msg: A :py:class:`~py2p.base.Message` handler: A :py:class:`~py2p.chord.ChordConnection` Returns: Either ``True`` or ``None`` """packets=msg.packetsifpackets[0]==flags.peers:new_peers=packets[1]defis_prev(id):# type: (Union[bytes, bytearray, str]) -> boolreturndistance(b58decode_int(id),self.id_10)<=distance(self.prev.id_10,self.id_10)defis_next(id):# type: (Union[bytes, bytearray, str]) -> boolreturndistance(self.id_10,b58decode_int(id))<=distance(self.id_10,self.next.id_10)foraddr,idinnew_peers:iflen(tuple(self.outgoing))<max_outgoingoris_prev(id)oris_next(id):try:self.__connect(addr[0],addr[1],id)except:# pragma: no coverself.__print__("Could not connect to %s because\n%s"%(addr,format_exc()),level=1)continuereturnTruereturnNone

def__handle_retrieved(self,msg,handler):# type: (ChordSocket, Message, BaseConnection) -> Union[bool, None]"""This callback is used to deal with response signals. Its two primary jobs are: - if it was your request, send the deferred message - if it was someone else's request, relay the information Args: msg: A :py:class:`~py2p.base.Message` handler: A :py:class:`~py2p.chord.ChordConnection` Returns: Either ``True`` or ``None`` """packets=msg.packetsifpackets[0]==flags.retrieved:self.__print__("Response received for request id %s"%packets[1],level=1)ifself.requests.get((packets[1],packets[2])):value=cast(awaiting_value,self.requests.get((packets[1],packets[2])))value.value=packets[3]ifvalue.callback:value.callback_method(packets[1],packets[2])returnTruereturnNonedef__handle_retrieve(self,msg,handler):# type: (ChordSocket, Message, BaseConnection) -> Union[bool, None]"""This callback is used to deal with data retrieval signals. Its two primary jobs are: - respond with data you possess - if you don't possess it, make a request with your closest peer to that key Args: msg: A :py:class:`~py2p.base.Message` handler: A :py:class:`~py2p.chord.ChordConnection` Returns: Either ``True`` or ``None`` """packets=msg.packetsifpackets[0]==flags.retrieve:ifsanitize_packet(packets[1])inhashes:val=self.__lookup(sanitize_packet(packets[1]),b58decode_int(packets[2]),cast(ChordConnection,handler))ifval.valueisnotNone:self.__print__(val.value,level=1)handler.send(flags.whisper,flags.retrieved,packets[1],packets[2],cast(MsgPackable,val.value))else:handler.send(flags.whisper,flags.retrieved,packets[1],packets[2],None)returnTruereturnNonedef__handle_store(self,msg,handler):# type: (ChordSocket, Message, BaseConnection) -> Union[bool, None]"""This callback is used to deal with data storage signals. Its two primary jobs are: - store data in keys you're responsible for - if you aren't responsible, make a request with your closest peer to that key Args: msg: A :py:class:`~py2p.base.Message` handler: A :py:class:`~py2p.chord.ChordConnection` Returns: Either ``True`` or ``None`` """packets=msg.packetsifpackets[0]==flags.store:method=packets[1]key=b58decode_int(packets[2])self.__store(method,key,packets[3])returnTruereturnNonedef__handle_delta(self,msg,handler):# type: (ChordSocket, Message, BaseConnection) -> Union[bool, None]"""This callback is used to deal with delta storage signals. Its primary job is: - update the mapping in a given key Args: msg: A :py:class:`~py2p.base.Message` handler: A :py:class:`~py2p.chord.ChordConnection` Returns: Either ``True`` or ``None`` """packets=msg.packetsifpackets[0]==flags.delta:method=packets[1]key=b58decode_int(packets[2])self.__delta(method,key,packets[3])returnTruereturnNone

[docs]defdump_data(self,start,end):# type: (ChordSocket, int, int) -> Dict[bytes, Dict[int, MsgPackable]]"""Args: start: An :py:class:`int` which indicates the start of the desired key range. ``0`` will get all data. end: An :py:class:`int` which indicates the end of the desired key range. ``None`` will get all data. Returns: A nested :py:class:`dict` containing your data from start to end """ret=dict(((method,{})formethodinhashes))# type: Dict[bytes, Dict[int, MsgPackable]]self.__print__("Entering dump_data",level=1)formethod,tableinself.data.items():forkey,valueintable.items():ifdistance(start,key)<distance(end,key):self.__print__(method,key,level=6)ret[method][key]=valuereturnret

def__lookup(self,method,key,handler=None):# type: (ChordSocket, bytes, int, ChordConnection) -> awaiting_value"""Looks up the value at a given hash function and key. This method deals with just *one* of the underlying hash tables. Args: method: The hash table that you wish to check. Must be a :py:class:`str` or :py:class:`bytes`-like object key: The key that you wish to check. Must be a :py:class:`int` or :py:class:`long` Returns: The value at said key in an :py:class:`py2p.utils.awaiting_value` object, which either contains or will eventually contain its result """node=self# type: Union[ChordSocket, BaseConnection]method=sanitize_packet(method)ifself.routing_table:node=self.find(key)elifself.awaiting_ids:node=choice(self.awaiting_ids)ifnodein(self,None):returnawaiting_value(self.data[method].get(key,None))else:node.send(flags.whisper,flags.retrieve,method,b58encode_int(key))ret=awaiting_value()ifhandler:ret.callback=handlerself.requests[method,b58encode_int(key)]=retreturnretdef__getitem(self,key,timeout=10):# type: (ChordSocket, Union[bytes, bytearray, str], int) -> MsgPackable"""Looks up the value at a given key. Under the covers, this actually checks five different hash tables, and returns the most common value given. Args: key: The key that you wish to check. Must be a :py:class:`str` or :py:class:`bytes`-like object timeout: The longest you would like to await a value (default: 10s) Returns: The value at said key Raises: socket.timeout: If the request goes partly-unanswered for >=timeout seconds KeyError: If the request is made for a key with no agreed-upon value Note: It's probably much better to use :py:func:`~py2p.chord.ChordSocket.get` """key=sanitize_packet(key)self._logger.debug('Getting value of {}'.format(key))keys=get_hashes(key)vals=[self.__lookup(method,x)formethod,xinzip(hashes,keys)]common,count=most_common(vals)iters=0limit=timeout//0.1while(commonisNoneorcount<=len(hashes)//2)anditers<limit:self.daemon.daemon.join(0.1)# type: ignore# This (correctly) errors if running in daemon, sleep doesn'titers+=1common,count=most_common(vals)ifcommonisnotNoneandcount>len(hashes)//2:returncommonelifiters==limit:raiseTimeoutException()raiseKeyError("This key does not have an agreed-upon value. ""values={}, count={}, majority={}, most common ={}".format(vals,count,len(hashes)//2+1,common))

[docs]def__getitem__(self,key):# type: (ChordSocket, Union[bytes, bytearray, str]) -> MsgPackable"""Looks up the value at a given key. Under the covers, this actually checks five different hash tables, and returns the most common value given. Args: key: The key that you wish to check. Must be a :py:class:`str` or :py:class:`bytes`-like object Returns: The value at said key Raises: socket.timeout: If the request goes partly-unanswered for >=timeout seconds KeyError: If the request is made for a key with no agreed-upon value Note: It's probably much better to use :py:func:`~py2p.chord.ChordSocket.get` """returnself.__getitem(key)

[docs]defgetSync(self,# type: ChordSocketkey,# type: Union[bytes, bytearray, str]ifError=None,# type: MsgPackabletimeout=10# type: int):# type: (...) -> MsgPackable"""Looks up the value at a given key. Under the covers, this actually checks five different hash tables, and returns the most common value given. Args: key: The key that you wish to check. Must be a :py:class:`str` or :py:class:`bytes`-like object ifError: The value you wish to return on exception (default: ``None``) timeout: The longest you would like to await a value (default: 10s) Returns: The value at said key, or the value at ifError if there's an :py:class:`Exception` Note: It's probably much better to use :py:func:`~py2p.chord.ChordSocket.get` """try:self._logger.debug('Getting value of {}, with fallback'.format(key,ifError))returnself.__getitem(key,timeout=timeout)except(KeyError,TimeoutException)ase:self._logger.debug('Did not get value of {}, so returning {}. Due to {}'.format(key,ifError,e))returnifError

[docs]defget(self,# type: ChordSocketkey,# type: Union[bytes, bytearray, str]ifError=None,# type: MsgPackabletimeout=10# type: int):# type: (...) -> Promise"""Looks up the value at a given key. Under the covers, this actually checks five different hash tables, and returns the most common value given. Args: key: The key that you wish to check. Must be a :py:class:`str` or :py:class:`bytes`-like object ifError: The value you wish to return on exception (default: ``None``) timeout: The longest you would like to await a value (default: 10s) Returns: A :py:class:`~async_promises.Promise` of the value at said key, or the value at ifError if there's an :py:class:`Exception` """@Promisedefresolver(resolve,reject):# type: (Callable, Callable) -> Noneresolve(self.getSync(key,ifError=ifError,timeout=timeout))self._logger.debug('Getting Promise of {}, with fallback'.format(key,ifError))returnresolver

def__store(self,method,key,value):# type: (ChordSocket, bytes, int, MsgPackable) -> None"""Updates the value at a given key. This method deals with just *one* of the underlying hash tables. Args: method: The hash table that you wish to check. Must be a :py:class:`str` or :py:class:`bytes`-like object key: The key that you wish to check. Must be a :py:class:`int` or :py:class:`long` value: The value you wish to put at this key. Must be a :py:class:`str` or :py:class:`bytes`-like object """node=self.find(key)# type: Union[ChordSocket, BaseConnection]method=sanitize_packet(method)ifself.leechingandnodeisselfandlen(self.awaiting_ids):node=choice(self.awaiting_ids)ifnodein(self,None):ifvalueisNone:delself.data[method][key]else:self.data[method][key]=valueelse:node.send(flags.whisper,flags.store,method,b58encode_int(key),value)

[docs]def__setitem__(self,# type: ChordSocketkey,# type: Union[bytes, bytearray, str]value# type: MsgPackable):# type: (...) -> None"""Updates the value at a given key. Under the covers, this actually uses five different hash tables, and updates the value in all of them. Args: key: The key that you wish to update. Must be a :py:class:`str` or :py:class:`bytes`-like object value: The value you wish to put at this key. Raises: TypeError: If your key is not :py:class:`bytes` -like OR if your value is not serializable. This means your value must be one of the following: - :py:class:`bool` - :py:class:`float` - :py:class:`int` (if ``2**64 > x > -2**63``) - :py:class:`str` - :py:class:`bytes` - :py:class:`unicode` - :py:class:`tuple` - :py:class:`list` - :py:class:`dict` (if all keys are :py:class:`unicode`) """_key=sanitize_packet(key)self._logger.debug('Setting value of {} to {}'.format(_key,value))keys=get_hashes(_key)formethod,xinzip(hashes,keys):self.__store(method,x,value)if_keynotinself.__keysandvalueisnotNone:self.__keys.add(_key)self.send(_key,type=flags.notify)elif_keyinself.__keysandvalueisNone:self.__keys.add(_key)self.send(_key,b'del',type=flags.notify)

def__delitem__(self,key):# type: (ChordSocket, Union[bytes, bytearray, str]) -> None_key=sanitize_packet(key)if_keynotinself.__keys:raiseKeyError(_key)self.set(_key,None)def__delta(self,method,key,delta):# type: (ChordSocket, bytes, int, MsgPackable) -> None"""Updates the value at a given key, using the supplied delta. This method deals with just *one* of the underlying hash tables. Args: method: The hash table that you wish to check. Must be a :py:class:`str` or :py:class:`bytes`-like object key: The key that you wish to check. Must be a :py:class:`int` or :py:class:`long` delta: The delta you wish to apply at this key. """node=self.find(key)# type: Union[ChordSocket, BaseConnection]method=sanitize_packet(method)ifself.leechingandnodeisselfandlen(self.awaiting_ids):node=choice(self.awaiting_ids)ifnodein(self,None):ifkeynotinself.data[method]:self.data[method][key]={}self.data[method][key].update(delta)# type: ignoreelse:node.send(flags.whisper,flags.delta,method,b58encode_int(key),delta)

[docs]defapply_delta(self,# type: ChordSocketkey,# type: Union[bytes, bytearray, str]delta# type: MsgPackable):# type: (...) -> Promise"""Updates a stored mapping with the given delta. This allows for more graceful handling of conflicting changes Args: key: The key you wish to apply a delta to. Must be a :py:class:`str` or :py:class:`bytes`-like object delta: A mapping which contains the keys you wish to update, and the values you wish to store Returns: A :py:class:`~async_promises.Promise` which yields the resulting data, or rejects with a :py:class:`TypeError` if the updated key does not store a mapping already. Raises: TypeError: If the updated key does not store a mapping already. """ifnotisinstance(delta,dict):raiseTypeError("Cannot apply delta if you feed a non-mapping")value=self.get(key)@Promisedefresolver(resolve,reject):# type: (Callable, Callable) -> Noneifnotisinstance(value.get(),dict)andvalue.get()isnotNone:reject(TypeError("Cannot apply delta to a non-mapping: {}".format(value.get())))else:_key=sanitize_packet(key)self._logger.debug('Applying a delta of {} to {}'.format(delta,_key))keys=get_hashes(_key)formethod,xinzip(hashes,keys):self.__delta(method,x,delta)ret=value.get()or{}ret.update(delta)resolve(ret)returnresolver

[docs]deffind(self,key):# type: (ChordSocket, int) -> Union[ChordSocket, ChordConnection]"""Finds the node which is responsible for a certain value. This does not necessarily mean that they are supposed to store that value, just that they are along your path to said node. Args: key: The key that you wish to check. Must be a :py:class:`int` or :py:class:`long` Returns: A :py:class:`~py2p.chord.ChordConnection` or this socket """ifnotself.leeching:ret=self# type: Union[ChordSocket, ChordConnection]gap=distance(self.id_10,key)else:ret=Nonegap=2**384forhandlerinself.data_storing:dist=distance(handler.id_10,key)ifdist<gap:ret=handlergap=distreturnret

[docs]deffind_prev(self,key):# type: (ChordSocket, int) -> Union[ChordSocket, ChordConnection]"""Finds the node which is farthest from a certain value. This is used to find a node's "predecessor"; the node it is supposed to delegate to in the event of a disconnections. Args: key: The key that you wish to check. Must be a :py:class:`int` or :py:class:`long` Returns: A :py:class:`~py2p.chord.ChordConnection` or this socket """ifnotself.leeching:ret=self# type: Union[ChordSocket, ChordConnection]gap=distance(key,self.id_10)else:ret=Nonegap=2**384forhandlerinself.data_storing:dist=distance(key,handler.id_10)ifdist<gap:ret=handlergap=distreturnret

[docs]defvalues(self):# type: (ChordSocket) -> Iterator[MsgPackable]"""Returns: an iterator of the underlying :py:class:`dict`'s values Raises: KeyError: If the key does not have a majority-recognized value socket.timeout: See KeyError """self._logger.debug('Retrieving all values')keys=self.keys()nxt=self.get(next(keys))forkeyinkeys:_nxt=self.get(key)ifnxt.get():yieldnxt.get()nxt=_nxtifnxt.get():yieldnxt.get()

[docs]defitems(self):# type: (ChordSocket) -> Iterator[Tuple[bytes, MsgPackable]]"""Returns: an iterator of the underlying :py:class:`dict`'s items Raises: KeyError: If the key does not have a majority-recognized value socket.timeout: See KeyError """self._logger.debug('Retrieving all items')keys=self.keys()p_key=next(keys)nxt=self.get(p_key)forkeyinkeys:_nxt=self.get(key)ifnxt.get():yield(p_key,nxt.get())p_key=keynxt=_nxtifnxt.get():yield(p_key,nxt.get())

[docs]defpop(self,key,*args):# type: (ChordSocket, bytes, *Any) -> MsgPackable"""Returns a value, with the side effect of deleting that association Args: Key: The key you wish to look up. Must be a :py:class:`str` or :py:class:`bytes`-like object ifError: The value you wish to return on :py:class:`Exception` (default: raise an :py:class:`Exception` ) Returns: The value of the supplied key, or ``ifError`` Raises: KeyError: If the key does not have a majority-recognized value socket.timeout: See KeyError """self._logger.debug('Popping key {}'.format(key))iflen(args):ret=self.getSync(key,args[0])ifret!=args[0]:delself[key]else:ret=self[key]delself[key]returnret

[docs]defpopitem(self):# type: (ChordSocket) -> Tuple[bytes, MsgPackable]"""Returns an association, with the side effect of deleting that association Returns: An arbitrary association Raises: KeyError: If the key does not have a majority-recognized value socket.timeout: See KeyError """self._logger.debug('Popping an item')key=next(self.keys())return(key,self.pop(key))

[docs]defcopy(self):# type: (ChordSocket) -> Dict[bytes, MsgPackable]"""Returns a :py:class:`dict` copy of this DHT .. warning:: This is a *very* slow operation. It's a far better idea to use :py:meth:`~py2p.chord.ChordSocket.items`, as this produces an iterator. That should even out lag times """self._logger.debug('Producing a dictionary copy')promises=[(key,self.get(key))forkeyinself.keys()]returndict((key,p.get())forkey,pinpromises)