howler-api 2.13.0.dev329__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of howler-api might be problematic. Click here for more details.
- howler/__init__.py +0 -0
- howler/actions/__init__.py +167 -0
- howler/actions/add_label.py +111 -0
- howler/actions/add_to_bundle.py +159 -0
- howler/actions/change_field.py +76 -0
- howler/actions/demote.py +160 -0
- howler/actions/example_plugin.py +104 -0
- howler/actions/prioritization.py +93 -0
- howler/actions/promote.py +147 -0
- howler/actions/remove_from_bundle.py +133 -0
- howler/actions/remove_label.py +111 -0
- howler/actions/transition.py +200 -0
- howler/api/__init__.py +249 -0
- howler/api/base.py +88 -0
- howler/api/socket.py +114 -0
- howler/api/v1/__init__.py +97 -0
- howler/api/v1/action.py +372 -0
- howler/api/v1/analytic.py +748 -0
- howler/api/v1/auth.py +382 -0
- howler/api/v1/borealis.py +101 -0
- howler/api/v1/configs.py +55 -0
- howler/api/v1/dossier.py +222 -0
- howler/api/v1/help.py +28 -0
- howler/api/v1/hit.py +1181 -0
- howler/api/v1/notebook.py +82 -0
- howler/api/v1/overview.py +191 -0
- howler/api/v1/search.py +715 -0
- howler/api/v1/template.py +206 -0
- howler/api/v1/tool.py +183 -0
- howler/api/v1/user.py +414 -0
- howler/api/v1/utils/__init__.py +0 -0
- howler/api/v1/utils/etag.py +84 -0
- howler/api/v1/view.py +288 -0
- howler/app.py +235 -0
- howler/common/README.md +144 -0
- howler/common/__init__.py +0 -0
- howler/common/classification.py +979 -0
- howler/common/classification.yml +107 -0
- howler/common/exceptions.py +167 -0
- howler/common/hexdump.py +48 -0
- howler/common/iprange.py +171 -0
- howler/common/loader.py +154 -0
- howler/common/logging/__init__.py +241 -0
- howler/common/logging/audit.py +138 -0
- howler/common/logging/format.py +38 -0
- howler/common/net.py +79 -0
- howler/common/net_static.py +1494 -0
- howler/common/random_user.py +316 -0
- howler/common/swagger.py +117 -0
- howler/config.py +64 -0
- howler/cronjobs/__init__.py +29 -0
- howler/cronjobs/retention.py +61 -0
- howler/cronjobs/rules.py +274 -0
- howler/cronjobs/view_cleanup.py +88 -0
- howler/datastore/README.md +112 -0
- howler/datastore/__init__.py +0 -0
- howler/datastore/bulk.py +72 -0
- howler/datastore/collection.py +2327 -0
- howler/datastore/constants.py +117 -0
- howler/datastore/exceptions.py +41 -0
- howler/datastore/howler_store.py +105 -0
- howler/datastore/migrations/fix_process.py +41 -0
- howler/datastore/operations.py +130 -0
- howler/datastore/schemas.py +90 -0
- howler/datastore/store.py +231 -0
- howler/datastore/support/__init__.py +0 -0
- howler/datastore/support/build.py +214 -0
- howler/datastore/support/schemas.py +90 -0
- howler/datastore/types.py +22 -0
- howler/error.py +91 -0
- howler/external/__init__.py +0 -0
- howler/external/generate_mitre.py +96 -0
- howler/external/generate_sigma_rules.py +31 -0
- howler/external/generate_tlds.py +47 -0
- howler/external/reindex_data.py +46 -0
- howler/external/wipe_databases.py +58 -0
- howler/gunicorn_config.py +25 -0
- howler/healthz.py +47 -0
- howler/helper/__init__.py +0 -0
- howler/helper/azure.py +50 -0
- howler/helper/discover.py +59 -0
- howler/helper/hit.py +236 -0
- howler/helper/oauth.py +247 -0
- howler/helper/search.py +92 -0
- howler/helper/workflow.py +110 -0
- howler/helper/ws.py +378 -0
- howler/odm/README.md +102 -0
- howler/odm/__init__.py +1 -0
- howler/odm/base.py +1504 -0
- howler/odm/charter.txt +146 -0
- howler/odm/helper.py +416 -0
- howler/odm/howler_enum.py +25 -0
- howler/odm/models/__init__.py +0 -0
- howler/odm/models/action.py +33 -0
- howler/odm/models/analytic.py +90 -0
- howler/odm/models/assemblyline.py +48 -0
- howler/odm/models/aws.py +23 -0
- howler/odm/models/azure.py +16 -0
- howler/odm/models/cbs.py +44 -0
- howler/odm/models/config.py +558 -0
- howler/odm/models/dossier.py +33 -0
- howler/odm/models/ecs/__init__.py +0 -0
- howler/odm/models/ecs/agent.py +17 -0
- howler/odm/models/ecs/autonomous_system.py +16 -0
- howler/odm/models/ecs/client.py +149 -0
- howler/odm/models/ecs/cloud.py +141 -0
- howler/odm/models/ecs/code_signature.py +27 -0
- howler/odm/models/ecs/container.py +32 -0
- howler/odm/models/ecs/dns.py +62 -0
- howler/odm/models/ecs/egress.py +10 -0
- howler/odm/models/ecs/elf.py +74 -0
- howler/odm/models/ecs/email.py +122 -0
- howler/odm/models/ecs/error.py +14 -0
- howler/odm/models/ecs/event.py +140 -0
- howler/odm/models/ecs/faas.py +24 -0
- howler/odm/models/ecs/file.py +84 -0
- howler/odm/models/ecs/geo.py +30 -0
- howler/odm/models/ecs/group.py +18 -0
- howler/odm/models/ecs/hash.py +16 -0
- howler/odm/models/ecs/host.py +17 -0
- howler/odm/models/ecs/http.py +37 -0
- howler/odm/models/ecs/ingress.py +12 -0
- howler/odm/models/ecs/interface.py +21 -0
- howler/odm/models/ecs/network.py +30 -0
- howler/odm/models/ecs/observer.py +45 -0
- howler/odm/models/ecs/organization.py +12 -0
- howler/odm/models/ecs/os.py +21 -0
- howler/odm/models/ecs/pe.py +17 -0
- howler/odm/models/ecs/process.py +216 -0
- howler/odm/models/ecs/registry.py +26 -0
- howler/odm/models/ecs/related.py +45 -0
- howler/odm/models/ecs/rule.py +51 -0
- howler/odm/models/ecs/server.py +24 -0
- howler/odm/models/ecs/threat.py +247 -0
- howler/odm/models/ecs/tls.py +58 -0
- howler/odm/models/ecs/url.py +51 -0
- howler/odm/models/ecs/user.py +57 -0
- howler/odm/models/ecs/user_agent.py +20 -0
- howler/odm/models/ecs/vulnerability.py +41 -0
- howler/odm/models/gcp.py +16 -0
- howler/odm/models/hit.py +356 -0
- howler/odm/models/howler_data.py +328 -0
- howler/odm/models/lead.py +33 -0
- howler/odm/models/localized_label.py +13 -0
- howler/odm/models/overview.py +16 -0
- howler/odm/models/pivot.py +40 -0
- howler/odm/models/template.py +24 -0
- howler/odm/models/user.py +83 -0
- howler/odm/models/view.py +34 -0
- howler/odm/random_data.py +888 -0
- howler/odm/randomizer.py +606 -0
- howler/patched.py +5 -0
- howler/plugins/__init__.py +25 -0
- howler/plugins/config.py +123 -0
- howler/remote/__init__.py +0 -0
- howler/remote/datatypes/README.md +355 -0
- howler/remote/datatypes/__init__.py +98 -0
- howler/remote/datatypes/counters.py +63 -0
- howler/remote/datatypes/events.py +66 -0
- howler/remote/datatypes/hash.py +206 -0
- howler/remote/datatypes/lock.py +42 -0
- howler/remote/datatypes/queues/__init__.py +0 -0
- howler/remote/datatypes/queues/comms.py +59 -0
- howler/remote/datatypes/queues/multi.py +32 -0
- howler/remote/datatypes/queues/named.py +93 -0
- howler/remote/datatypes/queues/priority.py +215 -0
- howler/remote/datatypes/set.py +118 -0
- howler/remote/datatypes/user_quota_tracker.py +54 -0
- howler/security/__init__.py +253 -0
- howler/security/socket.py +108 -0
- howler/security/utils.py +185 -0
- howler/services/__init__.py +0 -0
- howler/services/action_service.py +111 -0
- howler/services/analytic_service.py +128 -0
- howler/services/auth_service.py +323 -0
- howler/services/config_service.py +128 -0
- howler/services/dossier_service.py +252 -0
- howler/services/event_service.py +93 -0
- howler/services/hit_service.py +893 -0
- howler/services/jwt_service.py +158 -0
- howler/services/lucene_service.py +286 -0
- howler/services/notebook_service.py +119 -0
- howler/services/overview_service.py +44 -0
- howler/services/template_service.py +45 -0
- howler/services/user_service.py +330 -0
- howler/utils/__init__.py +0 -0
- howler/utils/annotations.py +28 -0
- howler/utils/chunk.py +38 -0
- howler/utils/dict_utils.py +200 -0
- howler/utils/isotime.py +17 -0
- howler/utils/list_utils.py +11 -0
- howler/utils/lucene.py +77 -0
- howler/utils/path.py +27 -0
- howler/utils/socket_utils.py +61 -0
- howler/utils/str_utils.py +256 -0
- howler/utils/uid.py +47 -0
- howler_api-2.13.0.dev329.dist-info/METADATA +71 -0
- howler_api-2.13.0.dev329.dist-info/RECORD +200 -0
- howler_api-2.13.0.dev329.dist-info/WHEEL +4 -0
- howler_api-2.13.0.dev329.dist-info/entry_points.txt +8 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from typing import TYPE_CHECKING, Generic, Optional, TypeVar, Union
|
|
6
|
+
|
|
7
|
+
from howler.common.exceptions import HowlerValueError
|
|
8
|
+
from howler.remote.datatypes import get_client, retry_call
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from redis import Redis
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_conditional_remove_script = """
|
|
15
|
+
local hash_name = KEYS[1]
|
|
16
|
+
local key_in_hash = ARGV[1]
|
|
17
|
+
local expected_value = ARGV[2]
|
|
18
|
+
local result = redis.call('hget', hash_name, key_in_hash)
|
|
19
|
+
if result == expected_value then
|
|
20
|
+
redis.call('hdel', hash_name, key_in_hash)
|
|
21
|
+
return 1
|
|
22
|
+
end
|
|
23
|
+
return 0
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
h_pop_script = """
|
|
28
|
+
local result = redis.call('hget', ARGV[1], ARGV[2])
|
|
29
|
+
if result then redis.call('hdel', ARGV[1], ARGV[2]) end
|
|
30
|
+
return result
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_limited_add = """
|
|
35
|
+
local set_name = KEYS[1]
|
|
36
|
+
local key = ARGV[1]
|
|
37
|
+
local value = ARGV[2]
|
|
38
|
+
local limit = tonumber(ARGV[3])
|
|
39
|
+
|
|
40
|
+
if redis.call('hlen', set_name) < limit then
|
|
41
|
+
return redis.call('hsetnx', set_name, key, value)
|
|
42
|
+
end
|
|
43
|
+
return nil
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
T = TypeVar("T")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class HashIterator(Generic[T]):
|
|
50
|
+
def __init__(self, hash_object: Hash[T]):
|
|
51
|
+
self.hash_object = hash_object
|
|
52
|
+
self.cursor = 0
|
|
53
|
+
self.buffer: list[tuple[str, T]] = []
|
|
54
|
+
self._load_next()
|
|
55
|
+
|
|
56
|
+
def __next__(self) -> tuple[str, T]:
|
|
57
|
+
while True:
|
|
58
|
+
if self.buffer:
|
|
59
|
+
return self.buffer.pop(0)
|
|
60
|
+
if self.cursor == 0:
|
|
61
|
+
raise StopIteration()
|
|
62
|
+
self._load_next()
|
|
63
|
+
|
|
64
|
+
def _load_next(self):
|
|
65
|
+
self.cursor, data = retry_call(self.hash_object.c.hscan, self.hash_object.name, self.cursor)
|
|
66
|
+
for key, value in data.items():
|
|
67
|
+
self.buffer.append((key.decode("utf-8"), json.loads(value)))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Hash(Generic[T]):
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
name: str,
|
|
74
|
+
host: Optional[Union[str, Redis]] = None,
|
|
75
|
+
port: Optional[int] = None,
|
|
76
|
+
):
|
|
77
|
+
self.c = get_client(host, port, False)
|
|
78
|
+
self.name = name
|
|
79
|
+
self._pop = self.c.register_script(h_pop_script)
|
|
80
|
+
self._limited_add = self.c.register_script(_limited_add)
|
|
81
|
+
self._conditional_remove = self.c.register_script(_conditional_remove_script)
|
|
82
|
+
|
|
83
|
+
def __iter__(self):
|
|
84
|
+
return HashIterator(self)
|
|
85
|
+
|
|
86
|
+
def __enter__(self):
|
|
87
|
+
return self
|
|
88
|
+
|
|
89
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
90
|
+
self.delete()
|
|
91
|
+
|
|
92
|
+
def add(self, key: str, value: T) -> int:
|
|
93
|
+
"""Add the (key, value) pair to the hash for new keys.
|
|
94
|
+
|
|
95
|
+
If a key already exists this operation doesn't add it.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
True if key has been added to the table, False otherwise.
|
|
99
|
+
"""
|
|
100
|
+
if isinstance(key, bytes):
|
|
101
|
+
raise HowlerValueError("Cannot use bytes for hashmap keys")
|
|
102
|
+
return retry_call(self.c.hsetnx, self.name, key, json.dumps(value))
|
|
103
|
+
|
|
104
|
+
def increment(self, key, increment: int = 1):
|
|
105
|
+
return int(retry_call(self.c.hincrby, self.name, key, increment))
|
|
106
|
+
|
|
107
|
+
def limited_add(self, key, value, size_limit):
|
|
108
|
+
"""Add a single value to the set, but only if that wouldn't make the set grow past a given size.
|
|
109
|
+
|
|
110
|
+
If the hash has hit the size limit returns None
|
|
111
|
+
Otherwise, returns the result of hsetnx (same as `add`)
|
|
112
|
+
"""
|
|
113
|
+
return retry_call(
|
|
114
|
+
self._limited_add,
|
|
115
|
+
keys=[self.name],
|
|
116
|
+
args=[key, json.dumps(value), size_limit],
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def exists(self, key: str) -> bool:
|
|
120
|
+
return retry_call(self.c.hexists, self.name, key)
|
|
121
|
+
|
|
122
|
+
def get(self, key: str) -> Optional[T]:
|
|
123
|
+
item = retry_call(self.c.hget, self.name, key)
|
|
124
|
+
if not item:
|
|
125
|
+
return item
|
|
126
|
+
return json.loads(item)
|
|
127
|
+
|
|
128
|
+
def keys(self) -> list[str]:
|
|
129
|
+
return [k.decode("utf-8") for k in retry_call(self.c.hkeys, self.name)]
|
|
130
|
+
|
|
131
|
+
def length(self):
|
|
132
|
+
return retry_call(self.c.hlen, self.name)
|
|
133
|
+
|
|
134
|
+
def items(self) -> dict[str, T]:
|
|
135
|
+
items = retry_call(self.c.hgetall, self.name)
|
|
136
|
+
if not isinstance(items, dict):
|
|
137
|
+
return {}
|
|
138
|
+
return {k.decode("utf-8"): json.loads(v) for k, v in items.items()}
|
|
139
|
+
|
|
140
|
+
def conditional_remove(self, key: str, value) -> bool:
|
|
141
|
+
return bool(
|
|
142
|
+
retry_call(
|
|
143
|
+
self._conditional_remove,
|
|
144
|
+
keys=[self.name],
|
|
145
|
+
args=[key, json.dumps(value)],
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def pop(self, key: str):
|
|
150
|
+
item = retry_call(self._pop, args=[self.name, key])
|
|
151
|
+
if not item:
|
|
152
|
+
return item
|
|
153
|
+
return json.loads(item)
|
|
154
|
+
|
|
155
|
+
def set(self, key: str, value: T):
|
|
156
|
+
if isinstance(key, bytes):
|
|
157
|
+
raise HowlerValueError("Cannot use bytes for hashmap keys")
|
|
158
|
+
return retry_call(self.c.hset, self.name, key, json.dumps(value))
|
|
159
|
+
|
|
160
|
+
def multi_set(self, data: dict[str, T]):
|
|
161
|
+
if any(isinstance(key, bytes) for key in data.keys()):
|
|
162
|
+
raise HowlerValueError("Cannot use bytes for hashmap keys")
|
|
163
|
+
encoded = {key: json.dumps(value) for key, value in data.items()}
|
|
164
|
+
return retry_call(self.c.hset, self.name, mapping=encoded)
|
|
165
|
+
|
|
166
|
+
def delete(self):
|
|
167
|
+
retry_call(self.c.delete, self.name)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class ExpiringHash(Hash):
|
|
171
|
+
def __init__(self, name, ttl=86400, host=None, port=None):
|
|
172
|
+
super(ExpiringHash, self).__init__(name, host, port)
|
|
173
|
+
self.ttl = ttl
|
|
174
|
+
self.last_expire_time: float = 0
|
|
175
|
+
|
|
176
|
+
def _conditional_expire(self):
|
|
177
|
+
if self.ttl:
|
|
178
|
+
ctime = time.time()
|
|
179
|
+
if ctime > self.last_expire_time + (self.ttl / 2):
|
|
180
|
+
retry_call(self.c.expire, self.name, self.ttl)
|
|
181
|
+
self.last_expire_time = ctime
|
|
182
|
+
|
|
183
|
+
def add(self, key, value):
|
|
184
|
+
rval = super(ExpiringHash, self).add(key, value)
|
|
185
|
+
self._conditional_expire()
|
|
186
|
+
return rval
|
|
187
|
+
|
|
188
|
+
def set(self, key, value):
|
|
189
|
+
rval = super(ExpiringHash, self).set(key, value)
|
|
190
|
+
self._conditional_expire()
|
|
191
|
+
return rval
|
|
192
|
+
|
|
193
|
+
def multi_set(self, data):
|
|
194
|
+
rval = super(ExpiringHash, self).multi_set(data)
|
|
195
|
+
self._conditional_expire()
|
|
196
|
+
return rval
|
|
197
|
+
|
|
198
|
+
def increment(self, key, increment=1):
|
|
199
|
+
rval = super(ExpiringHash, self).increment(key, increment)
|
|
200
|
+
self._conditional_expire()
|
|
201
|
+
return rval
|
|
202
|
+
|
|
203
|
+
def limited_add(self, key, value, size_limit):
|
|
204
|
+
rval = super(ExpiringHash, self).limited_add(key, value, size_limit)
|
|
205
|
+
self._conditional_expire()
|
|
206
|
+
return rval
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from howler.remote.datatypes import get_client, retry_call
|
|
2
|
+
from howler.utils.uid import get_random_id
|
|
3
|
+
|
|
4
|
+
lock_acquire_script = """
|
|
5
|
+
local lock_holder = ARGV[1]
|
|
6
|
+
local uuid = ARGV[2]
|
|
7
|
+
local timeout = ARGV[3]
|
|
8
|
+
if redis.call('setnx', lock_holder, uuid) == 1 then
|
|
9
|
+
redis.call('expire', lock_holder, timeout)
|
|
10
|
+
return true
|
|
11
|
+
end
|
|
12
|
+
return false
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
lock_release_script = """
|
|
16
|
+
local lock_holder = ARGV[1]
|
|
17
|
+
local lock_release = ARGV[2]
|
|
18
|
+
local uuid = ARGV[3]
|
|
19
|
+
if redis.call('get', lock_holder) == uuid then
|
|
20
|
+
redis.call('del', lock_holder)
|
|
21
|
+
redis.call('rpush', lock_release, uuid)
|
|
22
|
+
redis.call('expire', lock_release, 1)
|
|
23
|
+
end
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Lock(object):
|
|
28
|
+
def __init__(self, name, timeout, host=None, port=None):
|
|
29
|
+
self.uuid = get_random_id()
|
|
30
|
+
self.c = get_client(host, port, False)
|
|
31
|
+
self.lock_release = "-".join(("lock", str(timeout), name, "released"))
|
|
32
|
+
self.lock_holder = "-".join(("lock", str(timeout), name, "holder"))
|
|
33
|
+
self.timeout = timeout
|
|
34
|
+
self._acquire = self.c.register_script(lock_acquire_script)
|
|
35
|
+
self._release = self.c.register_script(lock_release_script)
|
|
36
|
+
|
|
37
|
+
def __enter__(self):
|
|
38
|
+
while not retry_call(self._acquire, args=[self.lock_holder, self.uuid, self.timeout]):
|
|
39
|
+
retry_call(self.c.blpop, self.lock_release, 1)
|
|
40
|
+
|
|
41
|
+
def __exit__(self, unused1, unused2, unused3):
|
|
42
|
+
retry_call(self._release, args=[self.lock_holder, self.lock_release, self.uuid])
|
|
File without changes
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import redis
|
|
4
|
+
|
|
5
|
+
from howler.remote.datatypes import decode, get_client, log, retry_call
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CommsQueue(object):
|
|
9
|
+
def __init__(self, names, host=None, port=None, private=False):
|
|
10
|
+
self.c = get_client(host, port, private)
|
|
11
|
+
self.p = retry_call(self.c.pubsub)
|
|
12
|
+
if not isinstance(names, list):
|
|
13
|
+
names = [names]
|
|
14
|
+
self.names = names
|
|
15
|
+
self._connected = False
|
|
16
|
+
|
|
17
|
+
def __enter__(self):
|
|
18
|
+
return self
|
|
19
|
+
|
|
20
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
21
|
+
retry_call(self.p.unsubscribe)
|
|
22
|
+
|
|
23
|
+
def _connect(self):
|
|
24
|
+
if not self._connected:
|
|
25
|
+
retry_call(self.p.subscribe, self.names)
|
|
26
|
+
self._connected = True
|
|
27
|
+
|
|
28
|
+
def close(self):
|
|
29
|
+
retry_call(self.p.close)
|
|
30
|
+
|
|
31
|
+
def listen(self, blocking=True):
|
|
32
|
+
retried = False
|
|
33
|
+
while True:
|
|
34
|
+
self._connect()
|
|
35
|
+
try:
|
|
36
|
+
if blocking:
|
|
37
|
+
i = self.p.listen()
|
|
38
|
+
v = next(i)
|
|
39
|
+
else:
|
|
40
|
+
v = self.p.get_message()
|
|
41
|
+
if v is None:
|
|
42
|
+
yield None
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
if isinstance(v, dict) and v.get("type", None) == "message":
|
|
46
|
+
data = decode(v.get("data", "null"))
|
|
47
|
+
yield data
|
|
48
|
+
except redis.ConnectionError:
|
|
49
|
+
log.warning("No connection to Redis, reconnecting...")
|
|
50
|
+
self._connected = False
|
|
51
|
+
retried = True
|
|
52
|
+
finally:
|
|
53
|
+
if self._connected and retried:
|
|
54
|
+
log.info("Reconnected to Redis!")
|
|
55
|
+
retried = False
|
|
56
|
+
|
|
57
|
+
def publish(self, message):
|
|
58
|
+
for name in self.names:
|
|
59
|
+
retry_call(self.c.publish, name, json.dumps(message))
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from howler.remote.datatypes import get_client, retry_call
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MultiQueue(object):
|
|
7
|
+
def __init__(self, host=None, port=None, private=False):
|
|
8
|
+
self.c = get_client(host, port, private)
|
|
9
|
+
|
|
10
|
+
def delete(self, name):
|
|
11
|
+
retry_call(self.c.delete, name)
|
|
12
|
+
|
|
13
|
+
def length(self, name):
|
|
14
|
+
return retry_call(self.c.llen, name)
|
|
15
|
+
|
|
16
|
+
def pop(self, name, blocking=True, timeout=0):
|
|
17
|
+
if blocking:
|
|
18
|
+
response = retry_call(self.c.blpop, name, timeout)
|
|
19
|
+
else:
|
|
20
|
+
response = retry_call(self.c.lpop, name)
|
|
21
|
+
|
|
22
|
+
if not response:
|
|
23
|
+
return response
|
|
24
|
+
|
|
25
|
+
if blocking:
|
|
26
|
+
return json.loads(response[1])
|
|
27
|
+
else:
|
|
28
|
+
return json.loads(response)
|
|
29
|
+
|
|
30
|
+
def push(self, name, *messages):
|
|
31
|
+
for message in messages:
|
|
32
|
+
retry_call(self.c.rpush, name, json.dumps(message))
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
from typing import Generic, Optional, TypeVar
|
|
4
|
+
|
|
5
|
+
from howler.common.exceptions import HowlerTypeError
|
|
6
|
+
from howler.remote.datatypes import get_client, retry_call
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NamedQueue(Generic[T]):
|
|
12
|
+
def __init__(self, name: str, host=None, port=None, private: bool = False, ttl: int = 0):
|
|
13
|
+
self.c = get_client(host, port, private)
|
|
14
|
+
self.name: str = name
|
|
15
|
+
self.ttl: int = ttl
|
|
16
|
+
self.last_expire_time: float = 0
|
|
17
|
+
|
|
18
|
+
def __enter__(self):
|
|
19
|
+
return self
|
|
20
|
+
|
|
21
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
22
|
+
self.delete()
|
|
23
|
+
|
|
24
|
+
def _conditional_expire(self):
|
|
25
|
+
if self.ttl:
|
|
26
|
+
ctime = time.time()
|
|
27
|
+
if ctime > self.last_expire_time + (self.ttl / 2):
|
|
28
|
+
retry_call(self.c.expire, self.name, self.ttl)
|
|
29
|
+
self.last_expire_time = ctime
|
|
30
|
+
|
|
31
|
+
def delete(self):
|
|
32
|
+
retry_call(self.c.delete, self.name)
|
|
33
|
+
|
|
34
|
+
def __len__(self):
|
|
35
|
+
return self.length()
|
|
36
|
+
|
|
37
|
+
def length(self):
|
|
38
|
+
return retry_call(self.c.llen, self.name)
|
|
39
|
+
|
|
40
|
+
def peek_next(self) -> Optional[T]:
|
|
41
|
+
response = retry_call(self.c.lrange, self.name, 0, 0)
|
|
42
|
+
|
|
43
|
+
if response:
|
|
44
|
+
return json.loads(response[0])
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
def pop_batch(self, size) -> list[T]:
|
|
48
|
+
response = retry_call(self.c.lpop, self.name, size)
|
|
49
|
+
|
|
50
|
+
if not response:
|
|
51
|
+
return []
|
|
52
|
+
return [json.loads(r) for r in response]
|
|
53
|
+
|
|
54
|
+
def pop(self, blocking: bool = True, timeout: int = 0) -> Optional[T]:
|
|
55
|
+
if blocking:
|
|
56
|
+
response = retry_call(self.c.blpop, self.name, timeout)
|
|
57
|
+
else:
|
|
58
|
+
response = retry_call(self.c.lpop, self.name)
|
|
59
|
+
|
|
60
|
+
if not response:
|
|
61
|
+
return response
|
|
62
|
+
|
|
63
|
+
if blocking:
|
|
64
|
+
return json.loads(response[1])
|
|
65
|
+
else:
|
|
66
|
+
return json.loads(response)
|
|
67
|
+
|
|
68
|
+
def push(self, *messages: T):
|
|
69
|
+
for message in messages:
|
|
70
|
+
retry_call(self.c.rpush, self.name, json.dumps(message))
|
|
71
|
+
self._conditional_expire()
|
|
72
|
+
|
|
73
|
+
def unpop(self, *messages: T):
|
|
74
|
+
"""Put all messages passed back at the head of the FIFO queue."""
|
|
75
|
+
for message in messages:
|
|
76
|
+
retry_call(self.c.lpush, self.name, json.dumps(message))
|
|
77
|
+
self._conditional_expire()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def select(*queues, **kw):
|
|
81
|
+
timeout = kw.get("timeout", 0)
|
|
82
|
+
if len(queues) < 1:
|
|
83
|
+
raise HowlerTypeError("At least one queue must be specified")
|
|
84
|
+
if any([not isinstance(q, NamedQueue) for q in queues]):
|
|
85
|
+
raise HowlerTypeError("Only NamedQueues supported")
|
|
86
|
+
|
|
87
|
+
c = queues[0].c
|
|
88
|
+
response = retry_call(c.blpop, [q.name for q in queues], timeout)
|
|
89
|
+
|
|
90
|
+
if not response:
|
|
91
|
+
return response
|
|
92
|
+
|
|
93
|
+
return response[0].decode("utf-8"), json.loads(response[1])
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from typing import Generic, Optional, TypeVar, Union
|
|
6
|
+
|
|
7
|
+
from howler.common.exceptions import HowlerTypeError
|
|
8
|
+
from howler.remote.datatypes import decode, get_client, retry_call
|
|
9
|
+
|
|
10
|
+
SORTING_KEY_LEN = 21
|
|
11
|
+
|
|
12
|
+
# Work around for inconsistency between ZRANGEBYSCORE and ZREMRANGEBYSCORE
|
|
13
|
+
# (No limit option available or we would just be using that directly)
|
|
14
|
+
#
|
|
15
|
+
# args:
|
|
16
|
+
# minimum score to pop
|
|
17
|
+
# maximum score to pop
|
|
18
|
+
# number of elements to skip before popping any
|
|
19
|
+
# max element count to pop
|
|
20
|
+
pq_dequeue_range_script = """
|
|
21
|
+
local unpack = table.unpack or unpack
|
|
22
|
+
local min_score = tonumber(ARGV[1]);
|
|
23
|
+
if min_score == nil then min_score = -math.huge end
|
|
24
|
+
local max_score = tonumber(ARGV[2]);
|
|
25
|
+
if max_score == nil then max_score = math.huge end
|
|
26
|
+
local rem_offset = tonumber(ARGV[3]);
|
|
27
|
+
local rem_limit = tonumber(ARGV[4]);
|
|
28
|
+
|
|
29
|
+
local entries = redis.call("zrangebyscore", KEYS[1], -max_score, -min_score, "limit", rem_offset, rem_limit);
|
|
30
|
+
if #entries > 0 then redis.call("zrem", KEYS[1], unpack(entries)) end
|
|
31
|
+
return entries
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
T = TypeVar("T")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PriorityQueue(Generic[T]):
|
|
38
|
+
def __init__(self, name, host=None, port=None, private=False):
|
|
39
|
+
self.c = get_client(host, port, private)
|
|
40
|
+
self._deque_range = self.c.register_script(pq_dequeue_range_script)
|
|
41
|
+
self.name = name
|
|
42
|
+
|
|
43
|
+
def __enter__(self):
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
47
|
+
self.delete()
|
|
48
|
+
|
|
49
|
+
def count(self, lowest, highest):
|
|
50
|
+
return retry_call(self.c.zcount, self.name, -highest, -lowest)
|
|
51
|
+
|
|
52
|
+
def delete(self):
|
|
53
|
+
retry_call(self.c.delete, self.name)
|
|
54
|
+
|
|
55
|
+
def length(self):
|
|
56
|
+
return retry_call(self.c.zcard, self.name)
|
|
57
|
+
|
|
58
|
+
def pop(self, num=None):
|
|
59
|
+
if num is not None and num <= 0:
|
|
60
|
+
return []
|
|
61
|
+
|
|
62
|
+
if num:
|
|
63
|
+
return [decode(s[0][SORTING_KEY_LEN:]) for s in retry_call(self.c.zpopmin, self.name, num)]
|
|
64
|
+
else:
|
|
65
|
+
ret_val = retry_call(self.c.zpopmin, self.name, 1)
|
|
66
|
+
if ret_val:
|
|
67
|
+
return decode(ret_val[0][0][SORTING_KEY_LEN:])
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
def blocking_pop(self, timeout=0, low_priority=False):
|
|
71
|
+
"""When only one item is requested, blocking is is possible."""
|
|
72
|
+
if low_priority:
|
|
73
|
+
result = retry_call(self.c.bzpopmax, self.name, timeout)
|
|
74
|
+
else:
|
|
75
|
+
result = retry_call(self.c.bzpopmin, self.name, timeout)
|
|
76
|
+
if result:
|
|
77
|
+
return decode(result[1][SORTING_KEY_LEN:])
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
def dequeue_range(self, lower_limit="", upper_limit="", skip=0, num=1) -> list[T]:
|
|
81
|
+
"""Dequeue a number of elements, within a specified range of scores.
|
|
82
|
+
|
|
83
|
+
Limits given are inclusive, can be made exclusive, see redis docs on how to format limits for that.
|
|
84
|
+
|
|
85
|
+
NOTE: lower/upper limit is negated+swapped in the lua script, no need to do it here
|
|
86
|
+
|
|
87
|
+
:param lower_limit: The score of all dequeued elements must be higher or equal to this.
|
|
88
|
+
:param upper_limit: The score of all dequeued elements must be lower or equal to this.
|
|
89
|
+
:param skip: In the range of available items to dequeue skip over this many.
|
|
90
|
+
:param num: Maximum number of elements to dequeue.
|
|
91
|
+
:return: list
|
|
92
|
+
"""
|
|
93
|
+
results = retry_call(
|
|
94
|
+
self._deque_range,
|
|
95
|
+
keys=[self.name],
|
|
96
|
+
args=[lower_limit, upper_limit, skip, num],
|
|
97
|
+
)
|
|
98
|
+
return [decode(res[SORTING_KEY_LEN:]) for res in results]
|
|
99
|
+
|
|
100
|
+
def push(self, priority: int, data: T, vip=None):
|
|
101
|
+
vip = 0 if vip else 9
|
|
102
|
+
value = f"{vip}{f'{int(time.time()*1000000):020}'}{json.dumps(data)}"
|
|
103
|
+
retry_call(self.c.zadd, self.name, {value: -priority})
|
|
104
|
+
return value
|
|
105
|
+
|
|
106
|
+
def rank(self, raw_value):
|
|
107
|
+
return retry_call(self.c.zrank, self.name, raw_value)
|
|
108
|
+
|
|
109
|
+
def remove(self, raw_value):
|
|
110
|
+
return retry_call(self.c.zrem, self.name, raw_value)
|
|
111
|
+
|
|
112
|
+
def unpush(self, num=None) -> Union[list[T], Optional[T]]:
|
|
113
|
+
if num is not None and num <= 0:
|
|
114
|
+
return []
|
|
115
|
+
|
|
116
|
+
if num:
|
|
117
|
+
return [decode(s[0][SORTING_KEY_LEN:]) for s in retry_call(self.c.zpopmax, self.name, num)]
|
|
118
|
+
else:
|
|
119
|
+
ret_val = retry_call(self.c.zpopmax, self.name, 1)
|
|
120
|
+
if ret_val:
|
|
121
|
+
return decode(ret_val[0][0][SORTING_KEY_LEN:])
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class UniquePriorityQueue(PriorityQueue):
|
|
126
|
+
"""A priority queue where duplicate entries will be merged."""
|
|
127
|
+
|
|
128
|
+
def __init__(self, name, host=None, port=None, private=False):
|
|
129
|
+
super().__init__(name, host, port, private)
|
|
130
|
+
|
|
131
|
+
def remove(self, data: str):
|
|
132
|
+
"""Remove a value from the priority queue."""
|
|
133
|
+
retry_call(self.c.zrem, self.name, json.dumps(data))
|
|
134
|
+
|
|
135
|
+
def push(self, priority: int, data, vip=None) -> int:
|
|
136
|
+
"""Add or update elements in the priority queue.
|
|
137
|
+
|
|
138
|
+
Existing elements will have their priority updated.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Number of _NEW_ elements in the queue after the operation.
|
|
142
|
+
"""
|
|
143
|
+
return retry_call(self.c.zadd, self.name, {json.dumps(data): -priority})
|
|
144
|
+
|
|
145
|
+
def pop(self, num=None):
|
|
146
|
+
if num is not None and num <= 0:
|
|
147
|
+
return []
|
|
148
|
+
|
|
149
|
+
if num:
|
|
150
|
+
return [decode(s[0]) for s in retry_call(self.c.zpopmin, self.name, num)]
|
|
151
|
+
else:
|
|
152
|
+
ret_val = retry_call(self.c.zpopmin, self.name, 1)
|
|
153
|
+
if ret_val:
|
|
154
|
+
return decode(ret_val[0][0])
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
def unpush(self, num=None):
|
|
158
|
+
if num is not None and num <= 0:
|
|
159
|
+
return []
|
|
160
|
+
|
|
161
|
+
if num:
|
|
162
|
+
return [decode(s[0]) for s in retry_call(self.c.zpopmax, self.name, num)]
|
|
163
|
+
else:
|
|
164
|
+
ret_val = retry_call(self.c.zpopmax, self.name, 1)
|
|
165
|
+
if ret_val:
|
|
166
|
+
return decode(ret_val[0][0])
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
def dequeue_range(self, lower_limit="", upper_limit="", skip=0, num=1):
|
|
170
|
+
"""Dequeue a number of elements, within a specified range of scores.
|
|
171
|
+
|
|
172
|
+
Limits given are inclusive, can be made exclusive, see redis docs on how to format limits for that.
|
|
173
|
+
|
|
174
|
+
NOTE: lower/upper limit is negated+swapped in the lua script, no need to do it here
|
|
175
|
+
|
|
176
|
+
:param lower_limit: The score of all dequeued elements must be higher or equal to this.
|
|
177
|
+
:param upper_limit: The score of all dequeued elements must be lower or equal to this.
|
|
178
|
+
:param skip: In the range of available items to dequeue skip over this many.
|
|
179
|
+
:param num: Maximum number of elements to dequeue.
|
|
180
|
+
:return: list
|
|
181
|
+
"""
|
|
182
|
+
results = retry_call(
|
|
183
|
+
self._deque_range,
|
|
184
|
+
keys=[self.name],
|
|
185
|
+
args=[lower_limit, upper_limit, skip, num],
|
|
186
|
+
)
|
|
187
|
+
return [decode(res) for res in results]
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def select(*queues, **kw):
|
|
191
|
+
timeout = kw.get("timeout", 0)
|
|
192
|
+
if len(queues) < 1:
|
|
193
|
+
raise HowlerTypeError("At least one queue must be specified")
|
|
194
|
+
if any([not isinstance(q, PriorityQueue) for q in queues]):
|
|
195
|
+
raise HowlerTypeError("Only NamedQueues supported")
|
|
196
|
+
|
|
197
|
+
c = queues[0].c
|
|
198
|
+
response = retry_call(c.bzpopmin, [q.name for q in queues], timeout)
|
|
199
|
+
|
|
200
|
+
if not response:
|
|
201
|
+
return response
|
|
202
|
+
|
|
203
|
+
return response[0].decode("utf-8"), json.loads(response[1][SORTING_KEY_LEN:])
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def length(*queues: PriorityQueue) -> list[int]:
|
|
207
|
+
"""Utility function for batch reading queue lengths."""
|
|
208
|
+
if not queues:
|
|
209
|
+
return []
|
|
210
|
+
pipeline = queues[0].c.pipeline(transaction=False)
|
|
211
|
+
|
|
212
|
+
for que in queues:
|
|
213
|
+
pipeline.zcard(que.name)
|
|
214
|
+
|
|
215
|
+
return retry_call(pipeline.execute)
|