# Path: motorway/hash_ring.py
# class HashRing(object):
# 
#     def __init__(self, nodes=None, weights=None):
#         """`nodes` is a list of objects that have a proper __str__ representation.
#         `weights` is dictionary that sets weights to the nodes.  The default
#         weight is that all nodes are equal.
#         """
#         self.ring = dict()
#         self._sorted_keys = []
# 
#         self.nodes = nodes
# 
#         if not weights:
#             weights = {}
#         self.weights = weights
# 
#         self._generate_circle()
# 
#     def _generate_circle(self):
#         """Generates the circle.
#         """
#         total_weight = 0
#         for node in self.nodes:
#             total_weight += self.weights.get(node, 1)
# 
#         for node in self.nodes:
#             weight = 1
# 
#             if node in self.weights:
#                 weight = self.weights.get(node)
# 
#             factor = math.floor((40*len(self.nodes)*weight) / total_weight);
# 
#             for j in range(0, int(factor)):
#                 b_key = self._hash_digest( '%s-%s' % (node, j) )
# 
#                 for i in range(0, 3):
#                     key = self._hash_val(b_key, lambda x: x+i*4)
#                     self.ring[key] = node
#                     self._sorted_keys.append(key)
# 
#         self._sorted_keys.sort()
# 
#     def get_node(self, string_key):
#         """Given a string key a corresponding node in the hash ring is returned.
# 
#         If the hash ring is empty, `None` is returned.
#         """
#         pos = self.get_node_pos(string_key)
#         if pos is None:
#             return None
#         return self.ring[ self._sorted_keys[pos] ]
# 
#     def get_node_pos(self, string_key):
#         """Given a string key a corresponding node in the hash ring is returned
#         along with it's position in the ring.
# 
#         If the hash ring is empty, (`None`, `None`) is returned.
#         """
#         if not self.ring:
#             return None
# 
#         key = self.gen_key(string_key)
# 
#         nodes = self._sorted_keys
#         pos = bisect(nodes, key)
# 
#         if pos == len(nodes):
#             return 0
#         else:
#             return pos
# 
#     def iterate_nodes(self, string_key, distinct=True):
#         """Given a string key it returns the nodes as a generator that can hold the key.
# 
#         The generator iterates one time through the ring
#         starting at the correct position.
# 
#         if `distinct` is set, then the nodes returned will be unique,
#         i.e. no virtual copies will be returned.
#         """
#         if not self.ring:
#             yield None, None
# 
#         returned_values = set()
#         def distinct_filter(value):
#             if str(value) not in returned_values:
#                 returned_values.add(str(value))
#                 return value
# 
#         pos = self.get_node_pos(string_key)
#         for key in self._sorted_keys[pos:]:
#             val = distinct_filter(self.ring[key])
#             if val:
#                 yield val
# 
#         for i, key in enumerate(self._sorted_keys):
#             if i < pos:
#                 val = distinct_filter(self.ring[key])
#                 if val:
#                     yield val
# 
#     def gen_key(self, key):
#         """Given a string key it returns a long value,
#         this long value represents a place on the hash ring.
# 
#         md5 is currently used because it mixes well.
#         """
#         b_key = self._hash_digest(key)
#         return self._hash_val(b_key, lambda x: x)
# 
#     def _hash_val(self, b_key, entry_fn):
#         return (( b_key[entry_fn(3)] << 24)
#                 |(b_key[entry_fn(2)] << 16)
#                 |(b_key[entry_fn(1)] << 8)
#                 | b_key[entry_fn(0)] )
# 
#     def _hash_digest(self, key):
#         m = md5_constructor()
#         m.update(key.encode('utf-8'))
#         return list(m.digest())


import random
import logging
from motorway.hash_ring import HashRing

logger = logging.getLogger(__name__)


class GroupingValueMissing(Exception):
    pass


class BaseGrouper(object):
    def __init__(self, destinations):
        self.destinations = destinations

    def get_destinations_for(self, value):
        raise NotImplementedError("Please implement get_destinations_for")


class HashRingGrouper(BaseGrouper):
    """
    Group messages based on a consistent hashing algorithm. e.g. a message with the same grouping key will always
    go to the same intersection
    """
    def __init__(self, *args, **kwargs):
        super(HashRingGrouper, self).__init__(*args, **kwargs)

# Path: motorway/hash_ring.py
# class HashRing(object):
# 
#     def __init__(self, nodes=None, weights=None):
#         """`nodes` is a list of objects that have a proper __str__ representation.
#         `weights` is dictionary that sets weights to the nodes.  The default
#         weight is that all nodes are equal.
#         """
#         self.ring = dict()
#         self._sorted_keys = []
# 
#         self.nodes = nodes
# 
#         if not weights:
#             weights = {}
#         self.weights = weights
# 
#         self._generate_circle()
# 
#     def _generate_circle(self):
#         """Generates the circle.
#         """
#         total_weight = 0
#         for node in self.nodes:
#             total_weight += self.weights.get(node, 1)
# 
#         for node in self.nodes:
#             weight = 1
# 
#             if node in self.weights:
#                 weight = self.weights.get(node)
# 
#             factor = math.floor((40*len(self.nodes)*weight) / total_weight);
# 
#             for j in range(0, int(factor)):
#                 b_key = self._hash_digest( '%s-%s' % (node, j) )
# 
#                 for i in range(0, 3):
#                     key = self._hash_val(b_key, lambda x: x+i*4)
#                     self.ring[key] = node
#                     self._sorted_keys.append(key)
# 
#         self._sorted_keys.sort()
# 
#     def get_node(self, string_key):
#         """Given a string key a corresponding node in the hash ring is returned.
# 
#         If the hash ring is empty, `None` is returned.
#         """
#         pos = self.get_node_pos(string_key)
#         if pos is None:
#             return None
#         return self.ring[ self._sorted_keys[pos] ]
# 
#     def get_node_pos(self, string_key):
#         """Given a string key a corresponding node in the hash ring is returned
#         along with it's position in the ring.
# 
#         If the hash ring is empty, (`None`, `None`) is returned.
#         """
#         if not self.ring:
#             return None
# 
#         key = self.gen_key(string_key)
# 
#         nodes = self._sorted_keys
#         pos = bisect(nodes, key)
# 
#         if pos == len(nodes):
#             return 0
#         else:
#             return pos
# 
#     def iterate_nodes(self, string_key, distinct=True):
#         """Given a string key it returns the nodes as a generator that can hold the key.
# 
#         The generator iterates one time through the ring
#         starting at the correct position.
# 
#         if `distinct` is set, then the nodes returned will be unique,
#         i.e. no virtual copies will be returned.
#         """
#         if not self.ring:
#             yield None, None
# 
#         returned_values = set()
#         def distinct_filter(value):
#             if str(value) not in returned_values:
#                 returned_values.add(str(value))
#                 return value
# 
#         pos = self.get_node_pos(string_key)
#         for key in self._sorted_keys[pos:]:
#             val = distinct_filter(self.ring[key])
#             if val:
#                 yield val
# 
#         for i, key in enumerate(self._sorted_keys):
#             if i < pos:
#                 val = distinct_filter(self.ring[key])
#                 if val:
#                     yield val
# 
#     def gen_key(self, key):
#         """Given a string key it returns a long value,
#         this long value represents a place on the hash ring.
# 
#         md5 is currently used because it mixes well.
#         """
#         b_key = self._hash_digest(key)
#         return self._hash_val(b_key, lambda x: x)
# 
#     def _hash_val(self, b_key, entry_fn):
#         return (( b_key[entry_fn(3)] << 24)
#                 |(b_key[entry_fn(2)] << 16)
#                 |(b_key[entry_fn(1)] << 8)
#                 | b_key[entry_fn(0)] )
# 
#     def _hash_digest(self, key):
#         m = md5_constructor()
#         m.update(key.encode('utf-8'))
#         return list(m.digest())


# Path: motorway/grouping.py
import random
import logging
from motorway.hash_ring import HashRing


logger = logging.getLogger(__name__)


class GroupingValueMissing(Exception):
    pass


class BaseGrouper(object):
    def __init__(self, destinations):
        self.destinations = destinations

    def get_destinations_for(self, value):
        raise NotImplementedError("Please implement get_destinations_for")


class HashRingGrouper(BaseGrouper):
    """
    Group messages based on a consistent hashing algorithm. e.g. a message with the same grouping key will always
    go to the same intersection
    """
    def __init__(self, *args, **kwargs):
        super(HashRingGrouper, self).__init__(*args, **kwargs)

        self.hash_ring = HashRing(self.destinations)