class_docstr: A data structure that contains DHT peers bucketed according to their distance to node_id.
  Follows Kademlia routing table as described in https://pdos.csail.mit.edu/~petar/papers/maymounkov-kademlia-lncs.pdf
  
  :param node_id: node id used to measure distance
  :param bucket_size: parameter $k$ from Kademlia paper Section 2.2
  :param depth_modulo: parameter $b$ from Kademlia paper Section 2.2.
  :note: you can find a more detailed description of parameters in DHTNode, see node.py
class_docstr_tok: ["A","data","structure","that","contains","DHT","peers","bucketed","according","to","their","distance","to","node_id",".","Follows","Kademlia","routing","table","as","described","in","https",":","\/\/pdos.csail.mit.edu\/~petar\/papers\/maymounkov-kademlia-lncs.pdf",":","param","node_id",":","node","id","used","to","measure","distance",":","param","bucket_size",":","parameter","$","k","$","from","Kademlia","paper","Section","2.2",":","param","depth_modulo",":","parameter","$","b","$","from","Kademlia","paper","Section","2.2.",":","note",":","you","can","find","a","more","detailed","description","of","parameters","in","DHTNode",",","see","node.py"]
docstr: Update routing table after an incoming request from :peer_id: or outgoing request to :peer_id:
  
  :returns: If we cannot add node_id to the routing table, return the least-recently-updated node (Section 2.2)
  :note: DHTProtocol calls this method for every incoming and outgoing request if there was a response.
    If this method returned a node to be ping-ed, the protocol will ping it to check and either move it to
    the start of the table or remove that node and replace it with
docstr_tok: ["Update","routing","table","after","an","incoming","request","from",":","peer_id",":","or","outgoing","request","to",":","peer_id",":",":","returns",":","If","we","can","not","add","node_id","to","the","routing","table",",","return","the","least-recently-updated","node","(","Section","2.2",")",":","note",":","DHTProtocol","calls","this","method","for","every","incoming","and","outgoing","request","if","there","was","a","response",".","If","this","method","returned","a","node","to","be","ping-ed",",","the","protocol","will","ping","it","to","check","and","either","move","it","to","the","start","of","the","table","or","remove","that","node","and","replace","it","with"]
code: def add_or_update_node(
      self, node_id: DHTID, peer_id: PeerID
  ) -> Optional[Tuple[DHTID, PeerID]]:
      """
      Update routing table after an incoming request from :peer_id: or outgoing request to :peer_id:
  
      :returns: If we cannot add node_id to the routing table, return the least-recently-updated node (Section 2.2)
      :note: DHTProtocol calls this method for every incoming and outgoing request if there was a response.
        If this method returned a node to be ping-ed, the protocol will ping it to check and either move it to
        the start of the table or remove that node and replace it with
      """
      bucket_index = self.get_bucket_index(node_id)
      bucket = self.buckets[bucket_index]
      store_success = bucket.add_or_update_node(node_id, peer_id)
  
      if (
          node_id in bucket.nodes_to_peer_id
          or node_id in bucket.replacement_nodes
      ):
          # if we added node to bucket or as a replacement, throw it into lookup dicts as well
          self.uid_to_peer_id[node_id] = peer_id
          self.peer_id_to_uid[peer_id] = node_id
  
      if not store_success:
          # Per section 4.2 of paper, split if the bucket has node's own id in its range
          # or if bucket depth is not congruent to 0 mod $b$
          if (
              bucket.has_in_range(self.node_id)
              or bucket.depth % self.depth_modulo != 0
          ):
              self.split_bucket(bucket_index)
              return self.add_or_update_node(node_id, peer_id)
  
          # The bucket is full and won't split further. Return a node to ping (see this method's docstring)
          return bucket.request_ping_node()
  
imports: from __future__ import annotations
  import hashlib
  import heapq
  import os
  import random
  from collections.abc import Iterable
  from itertools import chain
  from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Union
  from hivemind.p2p import PeerID
  from hivemind.utils import MSGPackSerializer, get_dht_time
label_desc: Use image node_id 3 for calling the RoutingTable obj's underlying member method code with example usage: obj.add_or_update_node(node_id, peer_id) and returns: bucket, self