-
Notifications
You must be signed in to change notification settings - Fork 54
/
counter.py
169 lines (143 loc) Β· 6.29 KB
/
counter.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# --------------------------------------------------------------------------- #
# counter.py #
# #
# Copyright Β© 2015-2020, Rajiv Bakulesh Shah, original author. #
# All rights reserved. #
# --------------------------------------------------------------------------- #
import collections
import contextlib
import itertools
from .dict import RedisDict
class RedisCounter(RedisDict, collections.Counter):
'Redis-backed container compatible with collections.Counter.'
# Method overrides:
def _populate(self, iterable=tuple(), *, sign=+1, **kwargs):
to_set = {}
try:
for key, value in iterable.items():
to_set[key] = sign * value
except AttributeError:
for key in iterable:
to_set[key] = to_set.get(key, self[key]) + sign
for key, value in kwargs.items():
original = self[key] if to_set.get(key, 0) == 0 else to_set[key]
to_set[key] = original + sign * value
to_set = {key: self[key] + value for key, value in to_set.items()}
to_set = {
self._encode(k): self._encode(v) for k, v in to_set.items()
}
if to_set:
self.redis.multi()
self.redis.hset(self.key, mapping=to_set)
def update(self, iterable=tuple(), **kwargs):
'Like dict.update() but add counts instead of replacing them. O(n)'
with self._watch(iterable):
self._populate(iterable, sign=+1, **kwargs)
def subtract(self, iterable=tuple(), **kwargs):
'Like dict.update() but subtracts counts instead of replacing them. O(n)'
with self._watch(iterable):
self._populate(iterable, sign=-1, **kwargs)
def __getitem__(self, key):
'c.__getitem__(key) <==> c.get(key, 0). O(1)'
try:
value = super().__getitem__(key)
except KeyError:
value = super().__missing__(key)
return value
def __delitem__(self, key):
'c.__delitem__(key) <==> del c[key]. O(1)'
with contextlib.suppress(KeyError):
super().__delitem__(key)
def __repr__(self):
'Return the string representation of a RedisCounter. O(n)'
items = self.most_common()
items = ("'{}': {}".format(key, value) for key, value in items)
items = ', '.join(items)
return self.__class__.__name__ + '{' + items + '}'
def _math_op(self, other, *, method):
with self._watch(other):
counter = collections.Counter(self.elements())
return method(counter, other)
def __add__(self, other):
"Return the addition our counts to other's counts, but keep only counts > 0. O(n)"
return self._math_op(other, method=collections.Counter.__add__)
def __sub__(self, other):
"Return the subtraction other's counts from our counts, but keep only counts > 0. O(n)"
return self._math_op(other, method=collections.Counter.__sub__)
def __or__(self, other):
"Return the max of our counts vs. other's counts (union), but keep only counts > 0. O(n)"
return self._math_op(other, method=collections.Counter.__or__)
def __and__(self, other):
"Return the min of our counts vs. other's counts (intersection) but keep only counts > 0. O(n)"
return self._math_op(other, method=collections.Counter.__and__)
def _unary_op(self, *, test_func, modifier_func):
counter = collections.Counter()
for key, value in self.items():
if test_func(value):
counter[key] = modifier_func(value)
return counter
def __pos__(self):
'Return our counts > 0. O(n)'
return self._unary_op(
test_func=lambda x: x > 0,
modifier_func=lambda x: x,
)
def __neg__(self):
'Return the absolute value of our counts < 0. O(n)'
return self._unary_op(
test_func=lambda x: x < 0,
modifier_func=lambda x: -x,
)
def _imath_op(self, other, *, sign=+1):
with self._watch(other):
to_set = {k: self[k] + sign * v for k, v in other.items()}
to_del = {k for k, v in to_set.items() if v <= 0}
to_del.update(
k for k, v in self.items() if k not in to_set and v <= 0
)
to_set = {
self._encode(k): self._encode(v) for k, v in to_set.items() if v
}
to_del = {self._encode(k) for k in to_del}
if to_set or to_del:
self.redis.multi()
if to_set:
self.redis.hset(self.key, mapping=to_set)
if to_del:
self.redis.hdel(self.key, *to_del)
return self
def __iadd__(self, other):
'Same as __add__(), but in-place. O(n)'
return self._imath_op(other, sign=+1)
def __isub__(self, other):
'Same as __sub__(), but in-place. O(n)'
return self._imath_op(other, sign=-1)
def _iset_op(self, other, *, method_name):
with self._watch(other):
to_set, to_del = {}, set()
for k in itertools.chain(self, other):
if getattr(self[k], method_name)(other[k]):
to_set[k] = self[k]
else:
to_set[k] = other[k]
if to_set[k] <= 0:
del to_set[k]
to_del.add(k)
if to_set or to_del:
self.redis.multi()
if to_set:
to_set = {
self._encode(k): self._encode(v)
for k, v in to_set.items()
}
self.redis.hset(self.key, mapping=to_set)
if to_del:
to_del = {self._encode(k) for k in to_del}
self.redis.hdel(self.key, *to_del)
return self
def __ior__(self, other):
'Same as __or__(), but in-place. O(n)'
return self._iset_op(other, method_name='__gt__')
def __iand__(self, other):
'Same as __and__(), but in-place. O(n)'
return self._iset_op(other, method_name='__lt__')